Changed source root directory

This commit is contained in:
2026-03-05 16:30:11 +01:00
parent dc85447ee1
commit 538f85d7a2
5868 changed files with 749734 additions and 99 deletions

View File

@@ -0,0 +1,31 @@
<?php
/**
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Ajax;
abstract class AbstractAjaxService
{
/**
* Init ajax calls
*
* @return void
*/
abstract public function init();
/**
* Add ajax action
*
* @param string $tag ajax tag name
* @param string $methodName method name
*
* @return bool Always returns true
*/
protected function addAjaxCall($tag, $methodName)
{
return add_action($tag, array($this, $methodName));
}
}

View File

@@ -0,0 +1,133 @@
<?php
/**
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Ajax;
use DUP_Handler;
use DUP_Log;
use DUP_Util;
use Duplicator\Libs\Snap\SnapIO;
use Duplicator\Libs\Snap\SnapUtil;
use Exception;
class AjaxWrapper
{
/**
* This function wrap a callback and return always a json well formatted output.
*
* check nonce and capability if passed and return a json with this format
* [
* success : bool
* data : [
* funcData : mixed // callback return data
* message : string // a message for jvascript func (for example an exception message)
* output : string // all normal output wrapped between ob_start and ob_get_clean
* // if $errorUnespectedOutput is true and output isn't empty the json return an error
* ]
* ]
*
* @param callable $callback callback function
* @param string $nonceaction if action is null don't verify nonce
* @param string $nonce nonce string
* @param string $capability if capability is null don't verify capability
* @param bool $errorUnespectedOutput if true thorw exception with unespected optput
*
* @return void
*/
public static function json(
$callback,
$nonceaction = null,
$nonce = null,
$capability = null,
$errorUnespectedOutput = true
) {
$error = false;
$result = array(
'funcData' => null,
'output' => '',
'message' => ''
);
ob_start();
try {
DUP_Handler::init_error_handler();
$nonce = SnapUtil::sanitizeNSCharsNewline($nonce);
if (is_null($nonceaction) || !wp_verify_nonce($nonce, $nonceaction)) {
DUP_Log::trace('Security issue');
throw new Exception('Security issue');
}
if (!is_null($capability)) {
DUP_Util::hasCapability($capability, DUP_Util::SECURE_ISSUE_THROW);
}
// execute ajax function
$result['funcData'] = call_user_func($callback);
} catch (Exception $e) {
$error = true;
$result['message'] = $e->getMessage();
}
$result['output'] = ob_get_clean();
if ($errorUnespectedOutput && !empty($result['output'])) {
$error = true;
}
if ($error) {
wp_send_json_error($result);
} else {
wp_send_json_success($result);
}
}
/**
* This function wrap a callback and start a chunked file download.
* The callback must return a file path.
*
* @param callable():false|array{path:string,name:string} $callback Callback function that return a file path for download or false on error
* @param string $nonceaction if action is null don't verify nonce
* @param string $nonce nonce string
* @param bool $errorUnespectedOutput if true thorw exception with unespected optput
*
* @return never
*/
public static function fileDownload(
$callback,
$nonceaction = null,
$nonce = null,
$errorUnespectedOutput = true
) {
ob_start();
try {
DUP_Handler::init_error_handler();
$nonce = SnapUtil::sanitizeNSCharsNewline($nonce);
if (!is_null($nonceaction) && !wp_verify_nonce($nonce, $nonceaction)) {
DUP_Log::trace('Security issue');
throw new Exception('Security issue');
}
// execute ajax function
if (($fileInfo = call_user_func($callback)) === false) {
throw new Exception('Error generating file');
}
if (!@file_exists($fileInfo['path'])) {
throw new Exception('File ' . $fileInfo['path'] . ' not found');
}
$result['output'] = ob_get_clean();
if ($errorUnespectedOutput && !empty($result['output'])) {
throw new Exception('Unespected output');
}
SnapIO::serveFileForDownload($fileInfo['path'], $fileInfo['name'], DUPLICATOR_BUFFER_READ_WRITE_SIZE);
} catch (Exception $e) {
DUP_Log::trace($e->getMessage());
SnapIO::serverError500();
}
}
}

View File

@@ -0,0 +1,80 @@
<?php
/**
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Ajax;
use DUP_Package;
use Duplicator\Ajax\AjaxWrapper;
use Duplicator\Views\DashboardWidget;
class ServicesDashboard extends AbstractAjaxService
{
/**
* Init ajax calls
*
* @return void
*/
public function init()
{
$this->addAjaxCall('wp_ajax_duplicator_dashboad_widget_info', 'dashboardWidgetInfo');
$this->addAjaxCall('wp_ajax_duplicator_dismiss_recommended_plugin', 'dismissRecommendedPlugin');
}
/**
* Set recovery callback
*
* @return array<string, mixed>
*/
public static function dashboardWidgetInfoCallback()
{
$result = array(
'isRunning' => DUP_Package::isPackageRunning(),
'lastBackupInfo' => DashboardWidget::getLastBackupString()
);
return $result;
}
/**
* Set recovery action
*
* @return void
*/
public function dashboardWidgetInfo()
{
AjaxWrapper::json(
array(__CLASS__, 'dashboardWidgetInfoCallback'),
'duplicator_dashboad_widget_info',
$_POST['nonce'],
'export'
);
}
/**
* Set dismiss recommended callback
*
* @return bool
*/
public static function dismissRecommendedPluginCallback()
{
return (update_user_meta(get_current_user_id(), DashboardWidget::RECOMMENDED_PLUGIN_DISMISSED_OPT_KEY, true) !== false);
}
/**
* Set recovery action
*
* @return void
*/
public function dismissRecommendedPlugin()
{
AjaxWrapper::json(
array(__CLASS__, 'dismissRecommendedPluginCallback'),
'duplicator_dashboad_widget_dismiss_recommended',
$_POST['nonce'],
'export'
);
}
}

View File

@@ -0,0 +1,430 @@
<?php
/**
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Ajax;
use Plugin_Upgrader;
use Duplicator\Ajax\AjaxWrapper;
use Duplicator\Views\EducationElements;
use Exception;
use Duplicator\Libs\OneClickUpgrade\UpgraderSkin;
use DUP_Log;
use DUP_Settings;
use Duplicator\Core\Controllers\ControllersManager;
use Duplicator\Core\Notifications\Notice;
use Duplicator\Libs\Snap\SnapUtil;
class ServicesEducation extends AbstractAjaxService
{
const OPTION_KEY_ONE_CLICK_UPGRADE_OTH = 'duplicator_one_click_upgrade_oth';
const AUTH_TOKEN_KEY_OPTION_AUTO_ACTIVE = 'duplicator_pro_auth_token_auto_active';
const DUPLICATOR_STORE_URL = "https://duplicator.com";
const REMOTE_SUBSCRIBE_URL = 'https://duplicator.com/?lite_email_signup=1';
/**
* Init ajax calls
*
* @return void
*/
public function init()
{
$this->addAjaxCall('wp_ajax_duplicator_settings_callout_cta_dismiss', 'dismissCalloutCTA');
$this->addAjaxCall('wp_ajax_duplicator_packages_bottom_bar_dismiss', 'dismissBottomBar');
$this->addAjaxCall('wp_ajax_duplicator_email_subscribe', 'setEmailSubscribed');
$this->addAjaxCall('wp_ajax_duplicator_generate_connect_oth', 'generateConnectOTH');
$this->addAjaxCall('wp_ajax_nopriv_duplicator_lite_run_one_click_upgrade', 'oneClickUpgrade');
$this->addAjaxCall('wp_ajax_duplicator_lite_run_one_click_upgrade', 'oneClickUpgrade');
$this->addAjaxCall('wp_ajax_duplicator_enable_usage_stats', 'enableUsageStats');
}
/**
* Set email subscribed
*
* @return bool
*/
public static function setEmailSubscribedCallback()
{
if (EducationElements::userIsSubscribed()) {
return true;
}
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL, FILTER_NULL_ON_FAILURE);
if (is_null($email)) {
throw new \Exception('Invalid email');
}
$response = wp_remote_post(self::REMOTE_SUBSCRIBE_URL, array(
'method' => 'POST',
'timeout' => 45,
'body' => array('email' => $email)
));
if (is_wp_error($response) || 200 !== wp_remote_retrieve_response_code($response)) {
$error_msg = $response->get_error_code() . ': ' . $response->get_error_message();
SnapUtil::errorLog($error_msg);
throw new \Exception($error_msg);
}
return (update_user_meta(get_current_user_id(), EducationElements::DUP_EMAIL_SUBSCRIBED_OPT_KEY, true) !== false);
}
/**
* Set recovery action
*
* @return void
*/
public function setEmailSubscribed()
{
AjaxWrapper::json(
array(__CLASS__, 'setEmailSubscribedCallback'),
'duplicator_email_subscribe',
$_POST['nonce'],
'export'
);
}
/**
* Set dismiss callout CTA callback
*
* @return bool
*/
public static function dismissCalloutCTACallback()
{
return (update_user_meta(get_current_user_id(), EducationElements::DUP_SETTINGS_FOOTER_CALLOUT_DISMISSED, true) !== false);
}
/**
* Dismiss callout CTA
*
* @return void
*/
public function dismissCalloutCTA()
{
AjaxWrapper::json(
array(__CLASS__, 'dismissCalloutCTACallback'),
'duplicator_settings_callout_cta_dismiss',
$_POST['nonce'],
'export'
);
}
/**
* Dismiss bottom bar callback
*
* @return bool
*/
public static function dismissBottomBarCallback()
{
return (update_user_meta(get_current_user_id(), EducationElements::DUP_PACKAGES_BOTTOM_BAR_DISMISSED, true) !== false);
}
/**
* Dismiss bottom bar
*
* @return void
*/
public function dismissBottomBar()
{
AjaxWrapper::json(
array(__CLASS__, 'dismissBottomBarCallback'),
'duplicator_packages_bottom_bar_dismiss',
$_POST['nonce'],
'export'
);
}
/**
* Generate OTH for connect flow
*
* @return void
*/
public function generateConnectOTH()
{
AjaxWrapper::json(
array(__CLASS__, 'generateConnectOTHCallback'),
'duplicator_generate_connect_oth',
SnapUtil::sanitizeTextInput(INPUT_POST, 'nonce'),
'export'
);
}
/**
* Generate OTH for connect flow callback
*
* @return array
* @throws Exception
*/
public static function generateConnectOTHCallback()
{
$oth = wp_generate_password(30, false, false);
$hashed_oth = self::hashOth($oth);
// Save HASHED OTH with TTL for security
$oth_data = array(
'token' => $hashed_oth, // Store hashed OTH for decryption
'created_at' => time(),
'expires_at' => time() + (10 * MINUTE_IN_SECONDS) // 10 minute expiration
);
delete_option(self::OPTION_KEY_ONE_CLICK_UPGRADE_OTH);
$ok = update_option(self::OPTION_KEY_ONE_CLICK_UPGRADE_OTH, $oth_data);
if (!$ok) {
throw new Exception("Problem saving security token.");
}
return array(
'success' => true,
'oth' => $hashed_oth,
'php_version' => phpversion(),
'wp_version' => get_bloginfo('version'),
'redirect_url' => admin_url('admin-ajax.php?action=duplicator_lite_run_one_click_upgrade')
);
}
/**
* Returh hashed OTH
*
* @param string $oth OTH
*
* @return string Hashed OTH
*/
protected static function hashOth($oth)
{
return hash_hmac('sha512', $oth, wp_salt());
}
/**
* Decrypt data using OTH-based key.
*
* @param string $encryptedData Base64 encoded encrypted data
* @param string $oth The OTH token
*
* @return string|false Decrypted data or false on failure
*/
protected static function decryptData($encryptedData, $oth)
{
try {
$encryption_key = substr(hash('sha256', $oth), 0, 32); // 32-byte key from OTH
$iv = substr($oth, 0, 16); // 16-byte IV from OTH
$encrypted = base64_decode($encryptedData);
return openssl_decrypt($encrypted, 'AES-256-CBC', $encryption_key, 0, $iv);
} catch (Exception $e) {
DUP_Log::trace("ERROR: Decryption failed: " . $e->getMessage());
return false;
}
}
/**
* Decrypt and parse encrypted package from service.
*
* @param string $encryptedPackage Base64 encoded encrypted package
* @param string $oth The OTH token
*
* @return array|false Parsed package data or false on failure
*/
protected static function decryptPackage($encryptedPackage, $oth)
{
$decrypted = self::decryptData($encryptedPackage, $oth);
if ($decrypted === false) {
return false;
}
$package = json_decode($decrypted, true);
if (json_last_error() !== JSON_ERROR_NONE) {
DUP_Log::trace("ERROR: Invalid JSON in decrypted package");
return false;
}
return $package;
}
/**
* Enable usage stats
*
* @return void
*/
public function enableUsageStats()
{
AjaxWrapper::json(
array(__CLASS__, 'enableUsageStatsCallback'),
'duplicator_enable_usage_stats',
SnapUtil::sanitizeTextInput(INPUT_POST, 'nonce'),
'manage_options'
);
}
/**
* Enable usage stats callback
*
* @return void
*/
public static function enableUsageStatsCallback()
{
$result = true;
if (DUP_Settings::Get('usage_tracking') !== true) {
DUP_Settings::setUsageTracking(true);
$result = DUP_Settings::Save();
}
return $result && self::setEmailSubscribedCallback();
}
/**
* Accepts encrypted package from remote endpoint, after validating the OTH.
*
* @return void
*/
public function oneClickUpgrade()
{
try {
// Get encrypted package from service
$encryptedPackage = sanitize_text_field($_REQUEST["package"] ?? '');
if (empty($encryptedPackage)) {
DUP_Log::trace("ERROR: No encrypted package received from service.");
throw new Exception("No encrypted package received from service");
}
// Get OTH data for validation
$oth_data = get_option(self::OPTION_KEY_ONE_CLICK_UPGRADE_OTH);
if (empty($oth_data) || !is_array($oth_data)) {
DUP_Log::trace("ERROR: Invalid OTH data structure.");
throw new Exception("Invalid security token");
}
// Check TTL expiration
if (time() > $oth_data['expires_at']) {
DUP_Log::trace("ERROR: OTH token expired.");
delete_option(self::OPTION_KEY_ONE_CLICK_UPGRADE_OTH);
throw new Exception("Security token expired");
}
// Decrypt package using OTH
$package = self::decryptPackage($encryptedPackage, $oth_data['token']);
if ($package === false) {
DUP_Log::trace("ERROR: Failed to decrypt package from service.");
throw new Exception("Invalid encrypted data");
}
// Extract data from decrypted package
$download_url = $package['download_url'] ?? '';
$auth_token = $package['auth_token'] ?? '';
if (empty($download_url)) {
DUP_Log::trace("ERROR: No download URL in decrypted package.");
throw new Exception("No download URL provided");
}
// Delete OTH so it cannot be replayed (single-use)
delete_option(self::OPTION_KEY_ONE_CLICK_UPGRADE_OTH);
// Save authentication token for Pro to use
if (!empty($auth_token)) {
delete_option(self::AUTH_TOKEN_KEY_OPTION_AUTO_ACTIVE);
update_option(self::AUTH_TOKEN_KEY_OPTION_AUTO_ACTIVE, $auth_token);
DUP_Log::trace("Authentication token saved for Pro activation.");
}
// Validate download URL format
if (!filter_var($download_url, FILTER_VALIDATE_URL)) {
DUP_Log::trace("ERROR: Invalid download URL format: " . $download_url);
throw new Exception("Invalid download URL format");
}
// Install Pro if not already installed
if (!is_dir(WP_PLUGIN_DIR . "/duplicator-pro")) {
DUP_Log::trace("Installing Pro using service-provided URL: " . $download_url);
// Request filesystem credentials
$url = esc_url_raw(add_query_arg(array('page' => 'duplicator-settings'), admin_url('admin.php')));
$creds = request_filesystem_credentials($url, '', false, false, null);
if (false === $creds || ! \WP_Filesystem($creds)) {
wp_send_json_error(array('message' => 'File system permissions error. Please check permissions and try again.'));
}
// Install the plugin
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
remove_action('upgrader_process_complete', array('Language_Pack_Upgrader', 'async_upgrade'), 20);
$installer = new Plugin_Upgrader(new UpgraderSkin());
$result = $installer->install($download_url);
if (is_wp_error($result)) {
DUP_Log::trace("ERROR: Plugin installation failed: " . $result->get_error_message());
throw new Exception('Plugin installation failed: ' . $result->get_error_message());
}
wp_cache_flush();
$plugin_basename = $installer->plugin_info();
if ($plugin_basename) {
$upgradeDir = dirname($plugin_basename);
if ($upgradeDir != "duplicator-pro" && !rename(WP_PLUGIN_DIR . "/" . $upgradeDir, WP_PLUGIN_DIR . "/duplicator-pro")) {
throw new Exception('Failed renaming plugin directory');
}
} else {
throw new Exception('Installation of upgrade version failed');
}
}
$newFolder = WP_PLUGIN_DIR . "/duplicator-pro";
if (!is_dir($newFolder)) {
DUP_Log::trace("ERROR: Duplicator Pro folder not found after installation");
throw new Exception('Pro plugin installation failed - folder not created');
}
// Deactivate Lite FIRST (critical for avoiding conflicts)
deactivate_plugins(DUPLICATOR_PLUGIN_PATH . "/duplicator.php");
// Create activation URL for Pro
$plugin = "duplicator-pro/duplicator-pro.php";
$pluginsAdminUrl = is_multisite() ? network_admin_url('plugins.php') : admin_url('plugins.php');
$activateProUrl = esc_url_raw(
add_query_arg(
array(
'action' => 'activate',
'plugin' => $plugin,
'_wpnonce' => wp_create_nonce("activate-plugin_$plugin")
),
$pluginsAdminUrl
)
);
// Redirect to WordPress activation URL
DUP_Log::trace("Pro installation successful. Redirecting to activation URL: " . $activateProUrl);
wp_safe_redirect($activateProUrl);
exit;
} catch (Exception $e) {
DUP_Log::trace("ERROR in oneClickUpgrade: " . $e->getMessage());
// Add error notice and redirect to settings page
Notice::error(
sprintf(__('Upgrade installation failed: %s. Please try again or install manually.', 'duplicator'), $e->getMessage()),
'one_click_upgrade_failed'
);
$settingsUrl = ControllersManager::getMenuLink(
ControllersManager::SETTINGS_SUBMENU_SLUG,
'general'
);
wp_safe_redirect($settingsUrl);
exit;
}
}
}

View File

@@ -0,0 +1,56 @@
<?php
/**
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Ajax;
use Duplicator\Ajax\AjaxWrapper;
use Duplicator\Utils\ExtraPlugins\ExtraPluginsMng;
class ServicesExtraPlugins extends AbstractAjaxService
{
/**
* Init ajax calls
*
* @return void
*/
public function init()
{
$this->addAjaxCall('wp_ajax_duplicator_install_extra_plugin', 'extraPluginInstall');
}
/**
* Install and activate or just activate plugin
*
* @return string
*/
public static function extraPluginInstallCallback()
{
$slug = filter_input(INPUT_POST, 'plugin', FILTER_SANITIZE_STRING);
$message = '';
if (!ExtraPluginsMng::getInstance()->install($slug, $message)) {
throw new \Exception($message);
}
return $message;
}
/**
* Addon plugin install action callback
*
* @return void
*/
public function extraPluginInstall()
{
AjaxWrapper::json(
array(__CLASS__, 'extraPluginInstallCallback'),
'duplicator_install_extra_plugin',
$_POST['nonce'],
'install_plugins'
);
}
}

View File

@@ -0,0 +1,69 @@
<?php
/**
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Ajax;
use Duplicator\Ajax\AjaxWrapper;
use Duplicator\Core\Notifications\Notifications;
class ServicesNotifications extends AbstractAjaxService
{
/**
* Init ajax calls
*
* @return void
*/
public function init()
{
$this->addAjaxCall('wp_ajax_duplicator_notification_dismiss', 'setDissmisedNotifications');
}
/**
* Dismiss notification
*
* @return bool
*/
public static function dismissNotifications()
{
$id = sanitize_key($_POST['id']);
$type = is_numeric($id) ? 'feed' : 'events';
$option = Notifications::getOption();
$option['dismissed'][] = $id;
$option['dismissed'] = array_unique($option['dismissed']);
// Remove notification.
if (!is_array($option[$type]) || empty($option[$type])) {
throw new \Exception('Notification type not set.');
}
foreach ($option[$type] as $key => $notification) {
if ((string)$notification['id'] === (string)$id) {
unset($option[$type][$key]);
break;
}
}
return update_option(Notifications::DUPLICATOR_NOTIFICATIONS_OPT_KEY, $option);
}
/**
* Set dismiss notification action
*
* @return void
*/
public function setDissmisedNotifications()
{
AjaxWrapper::json(
array(__CLASS__, 'dismissNotifications'),
Notifications::DUPLICATOR_NOTIFICATION_NONCE_KEY,
$_POST['nonce'],
'manage_options'
);
}
}

View File

@@ -0,0 +1,61 @@
<?php
/**
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Ajax;
use Duplicator\Ajax\AjaxWrapper;
use Duplicator\Libs\Snap\SnapURL;
use Duplicator\Libs\Snap\SnapUtil;
use Duplicator\Utils\Support\SupportToolkit;
class ServicesTools extends AbstractAjaxService
{
/**
* Init ajax calls
*
* @return void
*/
public function init()
{
$this->addAjaxCall('wp_ajax_duplicator_download_support_toolkit', 'downloadSupportToolkit');
}
/**
* Function to download diagnostic data
*
* @return never
*/
public function downloadSupportToolkit()
{
AjaxWrapper::fileDownload(
[
__CLASS__,
'downloadSupportToolkitCallback',
],
'duplicator_download_support_toolkit',
SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'nonce')
);
}
/**
* Function to create diagnostic data
*
* @return false|array{path:string,name:string}
*/
public static function downloadSupportToolkitCallback()
{
$domain = SnapURL::wwwRemove(SnapURL::parseUrl(network_home_url(), PHP_URL_HOST));
$result = [
'path' => SupportToolkit::getToolkit(),
'name' => SupportToolkit::SUPPORT_TOOLKIT_PREFIX .
substr(sanitize_file_name($domain), 0, 12) . '_' .
date('YmdHis') . '.zip',
];
return $result;
}
}

View File

@@ -0,0 +1,247 @@
<?php
namespace Duplicator\Controllers;
use Duplicator\Core\Controllers\ControllersManager;
use Duplicator\Core\Views\TplMng;
class AboutUsController
{
const ABOUT_US_TAB = 'about-info';
const GETTING_STARTED = 'getting-started';
const LITE_VS_PRO = 'lite-vs-pro';
const LITE_ENABLED_FULL = 'full';
const LITE_ENABLED_PARTIAL = 'partial';
const LITE_ENABLED_NONE = 'none';
/**
* Array containing all the lite vs pro features
*
* @var string[] $liteVsProfeatures
*/
public static $liteVsProfeatures = array();
/**
* Enqueue assets
*
* @return void
*/
public static function enqueues()
{
self::enqueueScripts();
self::enqueueStyles();
}
/**
* Enqueue scripts
*
* @return void
*/
public static function enqueueScripts()
{
wp_enqueue_script(
'duplicator-extra-plugins',
DUPLICATOR_PLUGIN_URL . "assets/js/extra-plugins.js",
array('jquery'),
DUPLICATOR_VERSION,
true
);
wp_localize_script(
'duplicator-extra-plugins',
'l10nDupExtraPlugins',
array(
'loading' => esc_html__('Loading...', 'duplicator'),
'failure' => esc_html__('Failure', 'duplicator'),
'active' => esc_html__('Active', 'duplicator'),
'activated' => esc_html__('Activated', 'duplicator'),
)
);
wp_localize_script(
'duplicator-extra-plugins',
'duplicator_extra_plugins',
array(
'ajax_url' => admin_url('admin-ajax.php'),
'extra_plugin_install_nonce' => wp_create_nonce('duplicator_install_extra_plugin'),
)
);
}
/**
* Enqueue styles
*
* @return void
*/
public static function enqueueStyles()
{
wp_enqueue_style(
'duplicator-about',
DUPLICATOR_PLUGIN_URL . "assets/css/about.css",
array(),
DUPLICATOR_VERSION
);
}
/**
* Render welcome screen
*
* @return void
*/
public static function render()
{
$levels = ControllersManager::getMenuLevels();
TplMng::getInstance()->render(
'admin_pages/about_us/tabs',
array(
'active_tab' => is_null($levels[ControllersManager::QUERY_STRING_MENU_KEY_L2]) ?
self::ABOUT_US_TAB : $levels[ControllersManager::QUERY_STRING_MENU_KEY_L2]
),
true
);
switch ($levels[ControllersManager::QUERY_STRING_MENU_KEY_L2]) {
case self::GETTING_STARTED:
TplMng::getInstance()->render('admin_pages/about_us/getting_started/main', array(), true);
break;
case self::LITE_VS_PRO:
TplMng::getInstance()->render('admin_pages/about_us/lite_vs_pro/main', array(), true);
break;
case self::ABOUT_US_TAB:
default:
TplMng::getInstance()->render('admin_pages/about_us/about_us/main', array(), true);
break;
}
}
/**
* Returns the lite vs pro features as an array
*
* @return array
*/
public static function getLiteVsProFeatures()
{
if (!empty(self::$liteVsProfeatures)) {
return self::$liteVsProfeatures;
}
self::$liteVsProfeatures = array(
array(
'title' => __('Backup Files & Database', 'duplicator'),
'lite_enabled' => self::LITE_ENABLED_FULL,
),
array(
'title' => __('File & Database Table Filters', 'duplicator'),
'lite_enabled' => self::LITE_ENABLED_FULL,
),
array(
'title' => __('Migration Wizard', 'duplicator'),
'lite_enabled' => self::LITE_ENABLED_FULL,
),
array(
'title' => __('Overwrite Live Site', 'duplicator'),
'lite_enabled' => self::LITE_ENABLED_FULL,
),
array(
'title' => __('Drag & Drop Installs', 'duplicator'),
'lite_enabled' => self::LITE_ENABLED_PARTIAL,
'lite_text' => __('Classic WordPress-less Installs Only', 'duplicator'),
'pro_text' => __(
'Drag and Drop migrations and site restores! Simply drag the bundled site archive to the site you wish to overwrite.',
'duplicator'
)
),
array(
'title' => __('Scheduled Backups', 'duplicator'),
'lite_enabled' => self::LITE_ENABLED_NONE,
'pro_text' => __(
'Ensure that your important data is regularly and consistently backed up, allowing for quick and efficient recovery in case of data loss.',
'duplicator'
)
),
array(
'title' => __('Recovery Points', 'duplicator'),
'lite_enabled' => self::LITE_ENABLED_NONE,
'pro_text' => __(
'Recovery Points provide protection against mistakes and bad updates by letting you quickly rollback your system to a known, good state.',
'duplicator'
)
),
array(
'title' => __('Cloud Storage', 'duplicator'),
'lite_enabled' => self::LITE_ENABLED_NONE,
'pro_text' => __(
'Back up to Dropbox, FTP, Google Drive, OneDrive, Amazon S3 or any S3-compatible storage service for safe storage.',
'duplicator'
)
),
array(
'title' => __('Larger Site Support', 'duplicator'),
'lite_enabled' => self::LITE_ENABLED_NONE,
'pro_text' => __(
'We\'ve developed a new way to package backups especially tailored for larger site. No server timeouts or other restrictions!',
'duplicator'
)
),
array(
'title' => __('Server-to-Server Import', 'duplicator'),
'lite_enabled' => self::LITE_ENABLED_NONE,
'pro_text' => __(
'Direct Server Transfers allow you to build an archive, then directly transfer it from the source ' .
'server to the destination server for a lightning fast migration!',
'duplicator'
)
),
array(
'title' => __('Multisite support', 'duplicator'),
'lite_enabled' => self::LITE_ENABLED_NONE,
'pro_text' => __(
'Supports multisite network backup & migration. Subsite As Standalone Install, Standalone ' .
'Import Into Multisite and Import Subsite Into Multisite',
'duplicator'
)
),
array(
'title' => __('Installer Branding', 'duplicator'),
'lite_enabled' => self::LITE_ENABLED_NONE,
'pro_text' => __('Create your own custom-configured WordPress site and "Brand" the installer file with your look and feel.', 'duplicator')
),
array(
'title' => __('Archive Encryption', 'duplicator'),
'lite_enabled' => self::LITE_ENABLED_NONE,
'pro_text' => __('Protect and secure the archive file with industry-standard AES-256 encryption!', 'duplicator')
),
array(
'title' => __('Advanced Backup Permissions', 'duplicator'),
'lite_enabled' => self::LITE_ENABLED_NONE,
'pro_text' => __(
'Enjoy granular access control to ensure only authorized users can perform these critical functions.',
'duplicator'
)
),
array(
'title' => __('Enhanced Features', 'duplicator'),
'lite_enabled' => self::LITE_ENABLED_NONE,
'pro_text' => __(
'Enhanced features include: Managed Hosting Support, Shared Database Support, Streamlined Installer, Email Alerts and more...',
'duplicator'
)
),
array(
'title' => __('Advanced Features', 'duplicator'),
'lite_enabled' => self::LITE_ENABLED_NONE,
'pro_text' => __(
'Advanced features included: Hourly Schedules, Custom Search & Replace, Migrate Duplicator Settings, Regenerate Salts and Developer Hooks',
'duplicator'
)
),
array(
'title' => __('Customer Support', 'duplicator'),
'lite_enabled' => self::LITE_ENABLED_NONE,
'lite_text' => __('Limited Support', 'duplicator'),
'pro_text' => __('Priority Support', 'duplicator')
)
);
return self::$liteVsProfeatures;
}
}

View File

@@ -0,0 +1,37 @@
<?php
/**
* Impost installer page controller
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Controllers;
use Duplicator\Core\Views\TplMng;
use Duplicator\Utils\Email\EmailSummary;
class EmailSummaryPreviewPageController
{
/**
* Init page
*
* @return void
*/
public static function init()
{
if (
!isset($_GET['page']) || $_GET['page'] !== EmailSummary::PEVIEW_SLUG
|| !is_admin()
|| !current_user_can('manage_options')
) {
return;
}
TplMng::getInstance()->render('mail/email_summary', array(
'packages' => EmailSummary::getInstance()->getPackagesInfo()
));
die;
}
}

View File

@@ -0,0 +1,50 @@
<?php
/**
* Impost installer page controller
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Controllers;
use Duplicator\Core\Controllers\ControllersManager;
use Duplicator\Core\Views\TplMng;
use Duplicator\Libs\Snap\SnapUtil;
class HelpPageController
{
const HELP_SLUG = 'duplicator-dynamic-help';
/**
* Class constructor
*
* @return void
*/
public static function init()
{
if (!ControllersManager::isCurrentPage(self::HELP_SLUG) || !is_admin()) {
return;
}
$tag = SnapUtil::sanitizeInput(INPUT_GET, 'tag', '');
TplMng::getInstance()->render(
"parts/help/main",
[
'tag' => $tag,
]
);
die;
}
/**
* Returns link to the help page
*
* @return string
*/
public static function getHelpLink()
{
return ControllersManager::getMenuLink(self::HELP_SLUG);
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace Duplicator\Controllers;
use DUP_UI_Dialog;
use Duplicator\Core\Views\TplMng;
class StorageController
{
/**
* Render storages page
*
* @return void
*/
public static function render()
{
TplMng::getInstance()->render('mocks/storage/storage', array(
'storages' => self::getStoragesData()
), true);
}
/**
* Fet storage alert dialog box
*
* @param string $utm_medium UTM medium for the upsell link
*
* @return DUP_UI_Dialog
*/
public static function getDialogBox($utm_medium)
{
require_once(DUPLICATOR_PLUGIN_PATH . '/classes/ui/class.ui.dialog.php');
$storageAlert = new DUP_UI_Dialog();
$storageAlert->title = __('Advanced Storage', 'duplicator');
$storageAlert->height = 600;
$storageAlert->width = 550;
$storageAlert->okText = '';
$storageAlert->message = TplMng::getInstance()->render('mocks/storage/popup', array(
'storages' => self::getStoragesData(),
'utm_medium' => $utm_medium,
), false);
$storageAlert->initAlert();
return $storageAlert;
}
/**
* Returns the storage data for the view
*
* @return array[]
*/
private static function getStoragesData()
{
return array(
array(
'title' => __('Amazon S3', 'duplicator'),
'label' => __('Amazon S3', 'duplicator'),
'iconUrl' => DUPLICATOR_PLUGIN_URL . 'assets/img/aws.svg',
),
array(
'title' => __('Google Drive', 'duplicator'),
'label' => __('Google Drive', 'duplicator'),
'iconUrl' => DUPLICATOR_PLUGIN_URL . 'assets/img/google-drive.svg',
),
array(
'title' => __('OneDrive', 'duplicator'),
'label' => __('OneDrive', 'duplicator'),
'iconUrl' => DUPLICATOR_PLUGIN_URL . 'assets/img/onedrive.svg',
),
array(
'title' => __('DropBox', 'duplicator'),
'label' => __('DropBox', 'duplicator'),
'iconUrl' => DUPLICATOR_PLUGIN_URL . 'assets/img/dropbox.svg',
),
array(
'title' => __('FTP/SFTP', 'duplicator'),
'label' => __('FTP/SFTP', 'duplicator'),
'fa-class' => 'fas fa-network-wired',
),
array(
'title' => __('Google Cloud Storage', 'duplicator'),
'label' => __('Google Cloud Storage', 'duplicator'),
'iconUrl' => DUPLICATOR_PLUGIN_URL . 'assets/img/google-cloud.svg',
),
array(
'title' => __('Back Blaze', 'duplicator'),
'label' => __('Back Blaze', 'duplicator'),
'iconUrl' => DUPLICATOR_PLUGIN_URL . 'assets/img/backblaze.svg',
),
array(
'title' => __('Cloudflare R2', 'duplicator'),
'label' => __('Cloudflare R2', 'duplicator'),
'iconUrl' => DUPLICATOR_PLUGIN_URL . 'assets/img/cloudflare.svg',
),
array(
'title' => __('DigitalOcean Spaces', 'duplicator'),
'label' => __('DigitalOcean Spaces', 'duplicator'),
'iconUrl' => DUPLICATOR_PLUGIN_URL . 'assets/img/digital-ocean.svg',
),
array(
'title' => __('Vultr Object Storage', 'duplicator'),
'label' => __('Vultr Object Storage', 'duplicator'),
'iconUrl' => DUPLICATOR_PLUGIN_URL . 'assets/img/vultr.svg',
),
array(
'title' => __('Dream Objects', 'duplicator'),
'label' => __('Dream Objects', 'duplicator'),
'iconUrl' => DUPLICATOR_PLUGIN_URL . 'assets/img/dreamhost.svg',
),
array(
'title' => __('Wasabi', 'duplicator'),
'label' => __('Wasabi', 'duplicator'),
'iconUrl' => DUPLICATOR_PLUGIN_URL . 'assets/img/wasabi.svg',
),
array(
'title' => __('S3-Compatible Provider', 'duplicator'),
'label' => __(
'S3-Compatible (Generic) Cloudian, Cloudn, Connectria, Constant, Exoscal, Eucalyptus, Nifty, Nimbula, Minio, etc...',
'duplicator'
),
'iconUrl' => DUPLICATOR_PLUGIN_URL . 'assets/img/aws.svg',
),
);
}
}

View File

@@ -0,0 +1,163 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2023, Snap Creek LLC
*/
namespace Duplicator\Controllers;
use Duplicator\Core\Controllers\ControllersManager;
use Duplicator\Core\Views\TplMng;
/**
* Welcome screen controller
*/
class WelcomeController
{
/**
* Hidden welcome page slug.
*
* @var string
*/
const SLUG = 'duplicator-getting-started';
/**
* Option determining redirect
*
* @var string
*/
const REDIRECT_OPT_KEY = 'duplicator_redirect_to_welcome';
/**
* Init.
*
* @return void
*/
public static function init()
{
// If user is in admin ajax or doing cron, return.
if (function_exists('wp_doing_ajax') && wp_doing_ajax()) {
return;
}
if (function_exists('wp_doing_cron') && wp_doing_cron()) {
return;
}
add_action('admin_menu', array(__CLASS__, 'register'));
add_action('admin_head', array(__CLASS__, 'hideMenu'));
add_action('admin_init', array(__CLASS__, 'redirect'), 9999);
}
/**
* Register the pages to be used for the Welcome screen (and tabs).
*
* These pages will be removed from the Dashboard menu, so they will
* not actually show. Sneaky, sneaky.
*
* @return void
*/
public static function register()
{
// Getting started - shows after installation.
$hook_suffix = add_dashboard_page(
esc_html__('Welcome to Duplicator', 'duplicator'),
esc_html__('Welcome to Duplicator', 'duplicator'),
'export',
self::SLUG,
array(__CLASS__, 'render')
);
add_action('admin_print_styles-' . $hook_suffix, array(__CLASS__, 'enqueues'));
}
/**
* Removed the dashboard pages from the admin menu.
*
* This means the pages are still available to us, but hidden.
*
* @return void
*/
public static function hideMenu()
{
remove_submenu_page('index.php', self::SLUG);
}
/**
* Welcome screen redirect.
*
* This function checks if a new install or update has just occurred. If so,
* then we redirect the user to the appropriate page.
*
* @return void
*/
public static function redirect()
{
if (!get_option(self::REDIRECT_OPT_KEY, false)) {
return;
}
/**
* Filter to disable the onboarding redirect.
*
* @since 1.5.11.1
*
* @param bool $disable True to disable the onboarding redirect.
*/
if (apply_filters('duplicator_disable_onboarding_redirect', false)) {
delete_option(self::REDIRECT_OPT_KEY);
return;
}
delete_option(self::REDIRECT_OPT_KEY);
wp_safe_redirect(admin_url('index.php?page=' . WelcomeController::SLUG));
exit;
}
/**
* Enqueue assets.
*
* @return void
*/
public static function enqueues()
{
wp_enqueue_style(
'dup-welcome',
DUPLICATOR_PLUGIN_URL . "assets/css/welcome.css",
array(),
DUPLICATOR_VERSION
);
wp_enqueue_script(
'duplicator-onboarding',
DUPLICATOR_PLUGIN_URL . "assets/js/onboarding.js",
array('jquery'),
DUPLICATOR_VERSION,
true
);
wp_localize_script(
'duplicator-onboarding',
'duplicator_onboarding',
array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce("duplicator_enable_usage_stats"),
'email' => wp_get_current_user()->user_email,
'redirect_url' => ControllersManager::getMenuLink(ControllersManager::PACKAGES_SUBMENU_SLUG)
)
);
wp_enqueue_style('dup-font-awesome');
wp_enqueue_script('dup-jquery-qtip');
wp_enqueue_style('dup-plugin-style');
wp_enqueue_style('dup-jquery-qtip');
}
/**
* Render welcome screen
*
* @return void
*/
public static function render()
{
TplMng::getInstance()->render('admin_pages/welcome/welcome', array(), true);
}
}

View File

@@ -0,0 +1,619 @@
<?php
/**
* Interface that collects the functions of initial duplicator Bootstrap
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Core;
use DUP_Constants;
use DUP_CTRL_Package;
use DUP_CTRL_Tools;
use DUP_CTRL_UI;
use DUP_Custom_Host_Manager;
use DUP_DB;
use DUP_LITE_Plugin_Upgrade;
use DUP_Log;
use DUP_Package;
use DUP_Settings;
use DUP_UI_Screen;
use Duplicator\Controllers\HelpPageController;
use Duplicator\Utils\Email\EmailSummaryBootstrap;
use Duplicator\Views\AdminNotices;
use DUP_Util;
use DUP_Web_Services;
use Duplicator\Ajax\ServicesDashboard;
use Duplicator\Ajax\ServicesEducation;
use Duplicator\Ajax\ServicesExtraPlugins;
use Duplicator\Ajax\ServicesTools;
use Duplicator\Controllers\AboutUsController;
use Duplicator\Controllers\EmailSummaryPreviewPageController;
use Duplicator\Controllers\WelcomeController;
use Duplicator\Core\Controllers\ControllersManager;
use Duplicator\Core\Notifications\Notice;
use Duplicator\Core\Notifications\NoticeBar;
use Duplicator\Core\Notifications\Notifications;
use Duplicator\Core\Notifications\Review;
use Duplicator\Core\Views\TplMng;
use Duplicator\Utils\CronUtils;
use Duplicator\Utils\ExtraPlugins\CrossPromotion;
use Duplicator\Utils\LinkManager;
use Duplicator\Views\DashboardWidget;
use Duplicator\Views\EducationElements;
use Duplicator\Utils\UsageStatistics\StatsBootstrap;
class Bootstrap
{
/**
* Init plugin
*
* @return void
*/
public static function init()
{
add_action('init', array(__CLASS__, 'hookWpInit'));
if (is_admin()) {
add_action('plugins_loaded', array(__CLASS__, 'update'));
add_action('plugins_loaded', array(__CLASS__, 'wpfrontIntegrate'));
add_action('init', array(__CLASS__, 'loadTextdomain'));
/* ========================================================
* ACTIVATE/DEACTIVE/UPDATE HOOKS
* ===================================================== */
register_activation_hook(DUPLICATOR_LITE_FILE, array('DUP_LITE_Plugin_Upgrade', 'onActivationAction'));
Unistall::registerHooks();
}
CronUtils::init();
StatsBootstrap::init();
EmailSummaryBootstrap::init();
}
/**
* Method called on wordpress hook init action
*
* @return void
*/
public static function hookWpInit()
{
if (is_admin()) {
$GLOBALS['CTRLS_DUP_CTRL_UI'] = new DUP_CTRL_UI();
$GLOBALS['CTRLS_DUP_CTRL_Tools'] = new DUP_CTRL_Tools();
$GLOBALS['CTRLS_DUP_CTRL_Package'] = new DUP_CTRL_Package();
if (is_multisite()) {
add_action('network_admin_menu', array(__CLASS__, 'menuInit'));
add_filter('network_admin_plugin_action_links', array(__CLASS__, 'manageLink'), 10, 2);
add_filter('network_admin_plugin_row_meta', array(__CLASS__, 'metaLinks'), 10, 2);
} else {
add_action('admin_menu', array(__CLASS__, 'menuInit'));
add_filter('plugin_action_links', array(__CLASS__, 'manageLink'), 10, 2);
add_filter('plugin_row_meta', array(__CLASS__, 'metaLinks'), 10, 2);
}
add_action('admin_init', array(__CLASS__, 'adminInit'));
add_action('in_admin_footer', array(__CLASS__, 'pluginFooter' ));
add_action('admin_footer', array(__CLASS__, 'adjustProMenuItemClass'));
add_action('admin_enqueue_scripts', array(__CLASS__, 'adminEqueueScripts'));
add_action('wp_ajax_duplicator_active_package_info', 'duplicator_active_package_info');
add_action('wp_ajax_duplicator_package_scan', 'duplicator_package_scan');
add_action('wp_ajax_duplicator_package_build', 'duplicator_package_build');
add_action('wp_ajax_duplicator_package_delete', 'duplicator_package_delete');
add_action('wp_ajax_duplicator_duparchive_package_build', 'duplicator_duparchive_package_build');
add_filter('admin_body_class', array(__CLASS__, 'addBodyClass'));
//Init Class
DUP_Custom_Host_Manager::getInstance()->init();
DUP_Settings::init();
DUP_Log::Init();
DUP_Util::init();
DUP_DB::init();
MigrationMng::init();
Notice::init();
NoticeBar::init();
Review::init();
AdminNotices::init();
DUP_Web_Services::init();
WelcomeController::init();
DashboardWidget::init();
EducationElements::init();
Notifications::init();
EmailSummaryPreviewPageController::init();
HelpPageController::init();
CrossPromotion::init();
$dashboardService = new ServicesDashboard();
$dashboardService->init();
$extraPlugin = new ServicesExtraPlugins();
$extraPlugin->init();
$educationService = new ServicesEducation();
$educationService->init();
$toolsServices = new ServicesTools();
$toolsServices->init();
}
}
/**
* Return admin body classes
*
* @param string $classes classes
*
* @return string
*/
public static function addBodyClass($classes)
{
if (ControllersManager::isDuplicatorPage()) {
$classes .= ' duplicator-pages';
}
return $classes;
}
/**
* Hooked into `plugins_loaded`. Routines used to update the plugin
*
* @return null
*/
public static function update()
{
if (DUPLICATOR_VERSION != get_option(DUP_LITE_Plugin_Upgrade::DUP_VERSION_OPT_KEY)) {
DUP_LITE_Plugin_Upgrade::onActivationAction();
// $snapShotDirPerm = substr(sprintf("%o", fileperms(DUP_Settings::getSsdirPath())),-4);
}
load_plugin_textdomain('duplicator');
}
/**
* Load text domain for translation
*
* @return void
*/
public static function loadTextdomain()
{
load_plugin_textdomain('duplicator', false, false);
}
/**
* User role editor integration
*
* @return void
*/
public static function wpfrontIntegrate()
{
if (DUP_Settings::Get('wpfront_integrate')) {
do_action('wpfront_user_role_editor_duplicator_init', array('export', 'manage_options', 'read'));
}
}
/**
* Hooked into `admin_init`. Init routines for all admin pages
*
* @return void
*/
public static function adminInit()
{
add_action('in_admin_header', array('Duplicator\\Views\\ViewHelper', 'adminLogoHeader'), 100);
/* CSS */
wp_register_style('dup-jquery-ui', DUPLICATOR_PLUGIN_URL . 'assets/css/jquery-ui.css', null, "1.14.1");
wp_register_style('dup-font-awesome', DUPLICATOR_PLUGIN_URL . 'assets/css/font-awesome/css/all.min.css', [], '6.4.2');
wp_register_style('dup-plugin-global-style', DUPLICATOR_PLUGIN_URL . 'assets/css/global_admin_style.css', null, DUPLICATOR_VERSION);
wp_register_style('dup-plugin-style', DUPLICATOR_PLUGIN_URL . 'assets/css/style.css', array('dup-plugin-global-style'), DUPLICATOR_VERSION);
wp_register_style('dup-jquery-qtip', DUPLICATOR_PLUGIN_URL . 'assets/js/jquery.qtip/jquery.qtip.min.css', null, '2.2.1');
wp_register_style('dup-parsley-style', DUPLICATOR_PLUGIN_URL . 'assets/css/parsley.css', null, '2.3.5');
/* JS */
wp_register_script('dup-handlebars', DUPLICATOR_PLUGIN_URL . 'assets/js/handlebars.min.js', array('jquery'), '4.0.10');
wp_register_script('dup-parsley', DUPLICATOR_PLUGIN_URL . 'assets/js/parsley.min.js', array('jquery'), '1.1.18');
wp_register_script('dup-jquery-qtip', DUPLICATOR_PLUGIN_URL . 'assets/js/jquery.qtip/jquery.qtip.min.js', array('jquery'), '2.2.1');
add_action('admin_head', [DUP_UI_Screen::class, 'getCustomCss']);
// Clean tmp folder
DUP_Package::not_active_files_tmp_cleanup();
$unhook_third_party_js = DUP_Settings::Get('unhook_third_party_js');
$unhook_third_party_css = DUP_Settings::Get('unhook_third_party_css');
if ($unhook_third_party_js || $unhook_third_party_css) {
add_action('admin_enqueue_scripts', array(__CLASS__, 'unhookThirdPartyAssets'), 99999, 1);
}
}
/**
* Hooked into `admin_menu`. Loads all of the wp left nav admin menus for Duplicator
*
* @return void
*/
public static function menuInit()
{
$menuLabel = apply_filters('duplicator_menu_label_duplicator', 'Duplicator');
//SVG Icon: See https://websemantics.uk/tools/image-to-data-uri-converter/
$hook_prefix = add_menu_page('Duplicator Plugin', $menuLabel, 'export', 'duplicator', null, DUP_Constants::ICON_SVG);
add_action('admin_print_scripts-' . $hook_prefix, array(__CLASS__, 'scripts'));
add_action('admin_print_styles-' . $hook_prefix, array(__CLASS__, 'styles'));
//Submenus are displayed in the same order they have in the array
$subMenuItems = self::getSubmenuItems();
foreach ($subMenuItems as $k => $subMenuItem) {
$pageTitle = apply_filters('duplicator_page_title_' . $subMenuItem['menu_slug'], $subMenuItem['page_title']);
$menuLabel = apply_filters('duplicator_menu_label_' . $subMenuItem['menu_slug'], $subMenuItem['menu_title']);
$subMenuItems[$k]['hook_prefix'] = add_submenu_page(
$subMenuItem['parent_slug'],
$pageTitle,
$menuLabel,
$subMenuItem['capability'],
$subMenuItem['menu_slug'],
$subMenuItem['callback'],
$k
);
add_action('admin_print_scripts-' . $subMenuItems[$k]['hook_prefix'], array(__CLASS__, 'scripts'));
if (isset($subMenuItem['enqueue_style_callback'])) {
add_action('admin_print_styles-' . $subMenuItems[$k]['hook_prefix'], $subMenuItem['enqueue_style_callback']);
}
add_action('admin_print_styles-' . $subMenuItems[$k]['hook_prefix'], array(__CLASS__, 'styles'));
}
}
/**
* Submenu datas
*
* @return array[]
*/
protected static function getSubmenuItems()
{
$proTitle = '<span id="dup-link-upgrade-highlight">' . __('Upgrade to Pro', 'duplicator') . '</span>';
$dupMenuNew = '<span class="dup-menu-new">&nbsp;' . __('NEW!', 'duplicator') . '</span>';
return array(
array(
'parent_slug' => 'duplicator',
'page_title' => __('Backups', 'duplicator'),
'menu_title' => __('Backups', 'duplicator'),
'capability' => 'export',
'menu_slug' => ControllersManager::MAIN_MENU_SLUG,
'callback' => function () {
include(DUPLICATOR_PLUGIN_PATH . 'views/packages/controller.php');
}
),
array(
'parent_slug' => 'duplicator',
'page_title' => __('Import Backups', 'duplicator'),
'menu_title' => __('Import Backups', 'duplicator'),
'capability' => 'export',
'menu_slug' => ControllersManager::IMPORT_SUBMENU_SLUG,
'callback' => function () {
TplMng::getInstance()->render('mocks/import/import');
},
'enqueue_style_callback' => array(__CLASS__, 'mocksStyles')
),
array(
'parent_slug' => 'duplicator',
'page_title' => __('Schedule Backups', 'duplicator'),
'menu_title' => __('Schedule Backups', 'duplicator') . $dupMenuNew,
'capability' => 'export',
'menu_slug' => ControllersManager::SCHEDULES_SUBMENU_SLUG,
'callback' => function () {
TplMng::getInstance()->render('mocks/schedule/schedules');
},
'enqueue_style_callback' => array(__CLASS__, 'mocksStyles')
),
array(
'parent_slug' => 'duplicator',
'page_title' => __('Storage', 'duplicator'),
'menu_title' => '<span class="dup-storages-menu-highlight">' . __('Storage', 'duplicator') . '</span>',
'capability' => 'export',
'menu_slug' => ControllersManager::STORAGE_SUBMENU_SLUG,
'callback' => array('Duplicator\\Controllers\\StorageController', 'render'),
'enqueue_style_callback' => array(__CLASS__, 'mocksStyles')
),
array(
'parent_slug' => 'duplicator',
'page_title' => __('Tools', 'duplicator'),
'menu_title' => __('Tools', 'duplicator'),
'capability' => 'manage_options',
'menu_slug' => ControllersManager::TOOLS_SUBMENU_SLUG,
'callback' => function () {
include(DUPLICATOR_PLUGIN_PATH . 'views/tools/controller.php');
},
'enqueue_style_callback' => function () {
AboutUsController::enqueues();
self::mocksStyles();
}
),
array(
'parent_slug' => 'duplicator',
'page_title' => __('Settings', 'duplicator'),
'menu_title' => __('Settings', 'duplicator'),
'capability' => 'manage_options',
'menu_slug' => ControllersManager::SETTINGS_SUBMENU_SLUG,
'callback' => function () {
include(DUPLICATOR_PLUGIN_PATH . 'views/settings/controller.php');
}
),
array(
'parent_slug' => 'duplicator',
'page_title' => __('About Duplicator', 'duplicator'),
'menu_title' => __('About Us', 'duplicator'),
'capability' => 'manage_options',
'menu_slug' => ControllersManager::ABOUT_US_SUBMENU_SLUG,
'callback' => array('Duplicator\\Controllers\\AboutUsController', 'render'),
'enqueue_style_callback' => array('Duplicator\\Controllers\\AboutUsController', 'enqueues')
),
array(
'parent_slug' => 'duplicator',
'page_title' => $proTitle,
'menu_title' => $proTitle,
'capability' => 'manage_options',
'menu_slug' => LinkManager::getCampaignUrl('admin-menu', 'Upgrade to Pro'),
'callback' => null,
)
);
}
/**
* Hooked into `admin_enqueue_scripts`. Init routines for all admin pages
*
* @access global
* @return null
*/
public static function adminEqueueScripts()
{
wp_enqueue_script('dup-global-script', DUPLICATOR_PLUGIN_URL . 'assets/js/global-admin-script.js', array('jquery'), DUPLICATOR_VERSION, true);
wp_localize_script(
'dup-global-script',
'dup_global_script_data',
array(
'nonce_admin_notice_to_dismiss' => wp_create_nonce('duplicator_admin_notice_to_dismiss'),
'nonce_settings_callout_to_dismiss' => wp_create_nonce('duplicator_settings_callout_cta_dismiss'),
'nonce_packages_bottom_bar_dismiss' => wp_create_nonce('duplicator_packages_bottom_bar_dismiss'),
'nonce_email_subscribe' => wp_create_nonce('duplicator_email_subscribe'),
'nonce_dashboard_widged_info' => wp_create_nonce("duplicator_dashboad_widget_info"),
'nonce_dashboard_widged_dismiss_recommended' => wp_create_nonce("duplicator_dashboad_widget_dismiss_recommended"),
'ajaxurl' => admin_url('admin-ajax.php')
)
);
wp_localize_script(
'dup-global-script',
'l10nDupGlobalScript',
array(
'subscribe' => esc_html__('Subscribe', 'duplicator'),
'subscribed' => esc_html__('Subscribed &#10003', 'duplicator'),
'subscribing' => esc_html__('Subscribing...', 'duplicator'),
'fail' => esc_html__('Failed &#10007', 'duplicator'),
'emailFail' => esc_html__('Email subscription failed with message: ', 'duplicator'),
)
);
wp_enqueue_script('dup-one-click-upgrade-script', DUPLICATOR_PLUGIN_URL . 'assets/js/one-click-upgrade.js', array('jquery'), DUPLICATOR_VERSION, true);
wp_localize_script(
'dup-one-click-upgrade-script',
'dup_one_click_upgrade_script_data',
array(
'nonce_generate_connect_oth' => wp_create_nonce('duplicator_generate_connect_oth'),
'ajaxurl' => admin_url('admin-ajax.php'),
'fail_notice_title' => __('Failed to connect to Duplicator Pro.', 'duplicator'),
'fail_notice_message_label' => __('Message: ', 'duplicator'),
'fail_notice_suggestion' => __('Please try again or contact support if the issue persists.', 'duplicator'),
)
);
wp_enqueue_script('dup-dynamic-help', DUPLICATOR_PLUGIN_URL . 'assets/js/dynamic-help.js', array('jquery'), DUPLICATOR_VERSION, true);
wp_localize_script(
'dup-dynamic-help',
'l10nDupDynamicHelp',
array(
'failMsg' => esc_html__('Failed to load help content!', 'duplicator')
)
);
wp_enqueue_script('dup-duplicator-tooltip', DUPLICATOR_PLUGIN_URL . 'assets/js/duplicator-tooltip.js', array('jquery'), DUPLICATOR_VERSION, true);
wp_localize_script(
'dup-duplicator-tooltip',
'l10nDupTooltip',
array(
'copy' => esc_html__('Copy to clipboard', 'duplicator'),
'copied' => esc_html__('copied to clipboard', 'duplicator'),
'copyUnable' => esc_html__('Unable to copy', 'duplicator')
)
);
wp_enqueue_style('dup-plugin-global-style');
}
/**
* Add the PRO badge to left sidebar menu item.
*
* @return void
*/
public static function adjustProMenuItemClass()
{
//Add to footer so it's applied on hovered item too
?>
<script>jQuery(function($) {
$('#dup-link-upgrade-highlight').parent().attr('target','_blank');
$('#dup-link-upgrade-highlight').closest('li').addClass('dup-submenu-upgrade-highlight')
});
</script>
<style>
.dup-submenu-upgrade-highlight,
.dup-submenu-upgrade-highlight a,
.dup-submenu-upgrade-highlight a span#dup-link-upgrade-highlight {
background-color: #1da867!important;
color: #fff!important;
border-color: #fff!important;
font-weight: 600!important;
}
.dup-storages-menu-highlight {
color: #27d584;
}
#adminmenu .dup-menu-new {
color: #f18200;
vertical-align: super;
font-size: 9px;
font-weight: 600;
padding-left: 2px;
}
</style>
<?php
}
/**
* Add the plugin footer
*
* @return void
*/
public static function pluginFooter()
{
if (!ControllersManager::isDuplicatorPage()) {
return;
}
TplMng::getInstance()->render('parts/plugin-footer');
}
/**
* Loads all required javascript libs/source for DupPro
*
* @return void
*/
public static function scripts()
{
wp_enqueue_script('jquery');
wp_enqueue_script('jquery-ui-core');
wp_enqueue_script('jquery-ui-progressbar');
wp_enqueue_script('dup-parsley');
wp_enqueue_script('dup-jquery-qtip');
}
/**
* Loads all CSS style libs/source for DupPro
*
* @return void
*/
public static function styles()
{
wp_enqueue_style('dup-jquery-ui');
wp_enqueue_style('dup-font-awesome');
wp_enqueue_style('dup-plugin-style');
wp_enqueue_style('dup-jquery-qtip');
}
/**
* Enqueue mock related styles
*
* @return void
*/
public static function mocksStyles()
{
wp_enqueue_style(
'dup-mocks-styles',
DUPLICATOR_PLUGIN_URL . 'assets/css/mocks.css',
array(),
DUPLICATOR_VERSION
);
}
/**
* Adds the manage link in the plugins list
*
* @param string[] $links links
* @param string $file file
*
* @return string The manage link in the plugins list
*/
public static function manageLink($links, $file)
{
static $this_plugin;
if (!$this_plugin) {
$this_plugin = plugin_basename(DUPLICATOR_LITE_FILE);
}
if ($file == $this_plugin) {
/*
$settings_link = '<a href="admin.php?page=duplicator">' . esc_html__("Manage", 'duplicator') . '</a>';
array_unshift($links, $settings_link);
*/
$upgrade_link = '<a style="color: #1da867;" class="dup-plugins-list-pro-upgrade" href="' .
esc_url(LinkManager::getCampaignUrl('plugin-actions-link')) . '" target="_blank">' .
'<strong style="display: inline;">' .
esc_html__("Upgrade to Pro", 'duplicator') .
'</strong></a>';
array_unshift($links, $upgrade_link);
}
return $links;
}
/**
* Adds links to the plugins manager page
*
* @param string[] $links links
* @param string $file file
*
* @return string The meta help link data for the plugins manager
*/
public static function metaLinks($links, $file)
{
$plugin = plugin_basename(DUPLICATOR_LITE_FILE);
// create link
if ($file == $plugin) {
$links[] = '<a href="admin.php?page=duplicator" title="' . esc_attr__('Manage Backups', 'duplicator') . '" style="">' .
esc_html__('Manage', 'duplicator') .
'</a>';
return $links;
}
return $links;
}
/**
* Remove all external styles and scripts coming from other plugins
* which may cause compatibility issue, especially with React
*
* @param string $hook hook
*
* @return void
*/
public static function unhookThirdPartyAssets($hook)
{
/*
$hook values in duplicator admin pages:
toplevel_page_duplicator
duplicator_page_duplicator-tools
duplicator_page_duplicator-settings
duplicator_page_duplicator-gopro
*/
if (strpos($hook, 'duplicator') !== false && strpos($hook, 'duplicator-pro') === false) {
$unhook_third_party_js = DUP_Settings::Get('unhook_third_party_js');
$unhook_third_party_css = DUP_Settings::Get('unhook_third_party_css');
$assets = array();
if ($unhook_third_party_css) {
$assets['styles'] = wp_styles();
}
if ($unhook_third_party_js) {
$assets['scripts'] = wp_scripts();
}
foreach ($assets as $type => $asset) {
foreach ($asset->registered as $handle => $dep) {
$src = $dep->src;
// test if the src is coming from /wp-admin/ or /wp-includes/ or /wp-fsqm-pro/.
if (
is_string($src) && // For some built-ins, $src is true|false
strpos($src, 'wp-admin') === false &&
strpos($src, 'wp-include') === false &&
// things below are specific to your plugin, so change them
strpos($src, 'duplicator') === false &&
strpos($src, 'woocommerce') === false &&
strpos($src, 'jetpack') === false &&
strpos($src, 'debug-bar') === false
) {
'scripts' === $type ? wp_dequeue_script($handle) : wp_dequeue_style($handle);
}
}
}
}
}
}

View File

@@ -0,0 +1,227 @@
<?php
/**
* Singlethon class that manages the various controllers of the administration of wordpress
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Core\Controllers;
use Duplicator\Libs\Snap\SnapUtil;
final class ControllersManager
{
const MAIN_MENU_SLUG = 'duplicator';
const PACKAGES_SUBMENU_SLUG = 'duplicator';
const IMPORT_SUBMENU_SLUG = 'duplicator-import';
const SCHEDULES_SUBMENU_SLUG = 'duplicator-schedules';
const STORAGE_SUBMENU_SLUG = 'duplicator-storage';
const ABOUT_US_SUBMENU_SLUG = 'duplicator-about-us';
const TEMPLATES_SUBMENU_SLUG = 'duplicator-templates';
const TOOLS_SUBMENU_SLUG = 'duplicator-tools';
const SETTINGS_SUBMENU_SLUG = 'duplicator-settings';
const DEBUG_SUBMENU_SLUG = 'duplicator-debug';
const UPSELL_SUBMENU_SLUG = 'duplicator-pro';
const QUERY_STRING_MENU_KEY_L1 = 'page';
const QUERY_STRING_MENU_KEY_L2 = 'tab';
const QUERY_STRING_MENU_KEY_L3 = 'subtab';
const QUERY_STRING_MENU_KEY_ACTION = 'action';
/**
* Return current menu levels
*
* @return string[]
*/
public static function getMenuLevels()
{
$result = array();
$exChars = '-_';
$result[self::QUERY_STRING_MENU_KEY_L1] = SnapUtil::sanitizeStrictInput(
SnapUtil::INPUT_REQUEST,
self::QUERY_STRING_MENU_KEY_L1,
null,
$exChars
);
$result[self::QUERY_STRING_MENU_KEY_L2] = SnapUtil::sanitizeStrictInput(
SnapUtil::INPUT_REQUEST,
self::QUERY_STRING_MENU_KEY_L2,
null,
$exChars
);
$result[self::QUERY_STRING_MENU_KEY_L3] = SnapUtil::sanitizeStrictInput(
SnapUtil::INPUT_REQUEST,
self::QUERY_STRING_MENU_KEY_L3,
null,
$exChars
);
return $result;
}
/**
* Return true if current page is a duplicator page
*
* @return boolean
*/
public static function isDuplicatorPage()
{
if (!is_admin()) {
return false;
}
switch (SnapUtil::sanitizeStrictInput(SnapUtil::INPUT_REQUEST, 'page', '', '-_ ')) {
case self::MAIN_MENU_SLUG:
case self::PACKAGES_SUBMENU_SLUG:
case self::IMPORT_SUBMENU_SLUG:
case self::SCHEDULES_SUBMENU_SLUG:
case self::STORAGE_SUBMENU_SLUG:
case self::ABOUT_US_SUBMENU_SLUG:
case self::TEMPLATES_SUBMENU_SLUG:
case self::TOOLS_SUBMENU_SLUG:
case self::SETTINGS_SUBMENU_SLUG:
case self::DEBUG_SUBMENU_SLUG:
case self::UPSELL_SUBMENU_SLUG:
return true;
default:
return false;
}
}
/**
* Return current action key or false if not exists
*
* @return string|bool
*/
public static function getAction()
{
return SnapUtil::sanitizeStrictInput(
SnapUtil::INPUT_REQUEST,
self::QUERY_STRING_MENU_KEY_ACTION,
false,
'-_'
);
}
/**
* Check current page
*
* @param string $page page key
* @param null|string $tabL1 tab level 1 key, null not check
* @param null|string $tabL2 tab level 12key, null not check
*
* @return boolean
*/
public static function isCurrentPage($page, $tabL1 = null, $tabL2 = null)
{
$levels = self::getMenuLevels();
if ($page !== $levels[self::QUERY_STRING_MENU_KEY_L1]) {
return false;
}
if (!is_null($tabL1) && $tabL1 !== $levels[self::QUERY_STRING_MENU_KEY_L2]) {
return false;
}
if (!is_null($tabL1) && !is_null($tabL2) && $tabL2 !== $levels[self::QUERY_STRING_MENU_KEY_L3]) {
return false;
}
return true;
}
/**
* Return current menu page URL
*
* @param array $extraData extra value in query string key=val
*
* @return string
*/
public static function getCurrentLink($extraData = array())
{
$levels = self::getMenuLevels();
return self::getMenuLink(
$levels[self::QUERY_STRING_MENU_KEY_L1],
$levels[self::QUERY_STRING_MENU_KEY_L2],
$levels[self::QUERY_STRING_MENU_KEY_L3],
$extraData
);
}
/**
* Return menu page URL
*
* @param string $page page slug
* @param string $subL2 tab level 1 slug, null not set
* @param string $subL3 tab level 2 slug, null not set
* @param array $extraData extra value in query string key=val
* @param bool $relative if true return relative path or absolute
*
* @return string
*/
public static function getMenuLink($page, $subL2 = null, $subL3 = null, $extraData = array(), $relative = true)
{
$data = $extraData;
$data[self::QUERY_STRING_MENU_KEY_L1] = $page;
if (!empty($subL2)) {
$data[self::QUERY_STRING_MENU_KEY_L2] = $subL2;
}
if (!empty($subL3)) {
$data[self::QUERY_STRING_MENU_KEY_L3] = $subL3;
}
if ($relative) {
$url = self_admin_url('admin.php', 'relative');
} else {
if (is_multisite()) {
$url = network_admin_url('admin.php');
} else {
$url = admin_url('admin.php');
}
}
return $url . '?' . http_build_query($data);
}
/**
* Return create package link
*
* @return string
*/
public static function getPackageBuildUrl()
{
return self::getMenuLink(
self::PACKAGES_SUBMENU_SLUG,
'new1',
null,
array(
'inner_page' => 'new1',
'_wpnonce' => wp_create_nonce('new1-package')
)
);
}
/**
* Return package detail link
*
* @param int $packageId package id
*
* @return string
*/
public static function getPackageDetailUrl($packageId)
{
return self::getMenuLink(
self::PACKAGES_SUBMENU_SLUG,
'detail',
null,
array(
'action' => 'detail',
'id' => $packageId
)
);
}
}

View File

@@ -0,0 +1,547 @@
<?php
/**
* Utility class managing th emigration data
*/
namespace Duplicator\Core;
use DUP_Archive;
use DUP_CTRL_Tools;
use DUP_Settings;
use DUP_Util;
use Duplicator\Libs\Snap\SnapWP;
use Duplicator\Libs\Snap\SnapIO;
use Duplicator\Utils\CachesPurge\CachesPurge;
use Duplicator\Utils\UsageStatistics\CommStats;
use Duplicator\Utils\UsageStatistics\PluginData;
use Duplicator\Views\AdminNotices;
use Error;
use Exception;
class MigrationMng
{
const HOOK_FIRST_LOGIN_AFTER_INSTALL = 'duplicator_first_login_after_install';
const HOOK_BOTTOM_MIGRATION_MESSAGE = 'duplicator_bottom_migration_message';
const FIRST_LOGIN_OPTION = 'duplicator_first_login_after_install';
const MIGRATION_DATA_OPTION = 'duplicator_migration_data';
const CLEAN_INSTALL_REPORT_OPTION = 'duplicator_clean_install_report';
/**
* messages to be displayed in the successful migration box
*
* @var array
*/
protected static $migrationCleanupReport = array(
'removed' => array(),
'stored' => array(),
'instFile' => array()
);
/**
* Init
*
* @return void
*/
public static function init()
{
add_action('admin_init', array(__CLASS__, 'adminInit'));
add_action(self::HOOK_FIRST_LOGIN_AFTER_INSTALL, function ($migrationData) {
DUP_Util::initSnapshotDirectory();
});
add_action(self::HOOK_FIRST_LOGIN_AFTER_INSTALL, array(__CLASS__, 'removeFirstLoginOption'));
add_action(self::HOOK_FIRST_LOGIN_AFTER_INSTALL, array(__CLASS__, 'renameInstallersPhpFiles'));
add_action(self::HOOK_FIRST_LOGIN_AFTER_INSTALL, array(__CLASS__, 'storeMigrationFiles'));
add_action(self::HOOK_FIRST_LOGIN_AFTER_INSTALL, array(__CLASS__, 'setDupSettingsAfterInstall'));
add_action(self::HOOK_FIRST_LOGIN_AFTER_INSTALL, array(__CLASS__, 'usageStatistics'));
// save cleanup report after actions
add_action(self::HOOK_FIRST_LOGIN_AFTER_INSTALL, array(__CLASS__, 'saveCleanupReport'), 100);
// LAST BEACAUSE MAKE A WP_REDIRECT
add_action(self::HOOK_FIRST_LOGIN_AFTER_INSTALL, array(__CLASS__, 'autoCleanFileAfterInstall'), 99999);
}
/**
* Admin Init function
*
* @return void
*/
public static function adminInit()
{
if (self::isFirstLoginAfterInstall()) {
add_action('current_screen', array(__CLASS__, 'wpAdminHook'), 99999);
update_option(AdminNotices::OPTION_KEY_MIGRATION_SUCCESS_NOTICE, true);
do_action(self::HOOK_FIRST_LOGIN_AFTER_INSTALL, self::getMigrationData());
}
}
/**
* Admin Hook function
*
* @return void
*/
public static function wpAdminHook()
{
if (!DUP_CTRL_Tools::isToolPage()) {
wp_redirect(DUP_CTRL_Tools::getDiagnosticURL(false));
exit;
}
}
/**
*
* @return boolean
*/
public static function isFirstLoginAfterInstall()
{
if (is_user_logged_in() && get_option(self::FIRST_LOGIN_OPTION, false)) {
if (is_multisite()) {
if (is_super_admin()) {
return true;
}
} else {
if (current_user_can('manage_options')) {
return true;
}
}
}
return false;
}
/**
* Purge all caches
*
* @return string[] // messages
*/
public static function purgeCaches()
{
if (
self::getMigrationData('restoreBackupMode') ||
in_array(self::getMigrationData('installType'), array(4,5,6,7)) //update with define when installerstat will be in namespace
) {
return array();
}
return CachesPurge::purgeAll();
}
/**
* Clean after install
*
* @param array $migrationData migration data
*
* @return void
*/
public static function usageStatistics($migrationData)
{
$migrationData = (object) $migrationData;
PluginData::getInstance()->updateFromMigrateData($migrationData);
CommStats::installerSend();
}
/**
*
* @param array $migrationData Migration data
*
* @return void
*/
public static function autoCleanFileAfterInstall($migrationData)
{
if ($migrationData == false || $migrationData['cleanInstallerFiles'] == false) {
return;
}
wp_redirect(DUP_CTRL_Tools::getCleanFilesAcrtionUrl(false));
exit;
}
/**
*
* @param array $migrationData Migration data
*
* @return void
*/
public static function setDupSettingsAfterInstall($migrationData)
{
flush_rewrite_rules(true);
}
/**
* return cleanup report
*
* @return array
*/
public static function getCleanupReport()
{
$option = get_option(self::CLEAN_INSTALL_REPORT_OPTION);
if (is_array($option)) {
self::$migrationCleanupReport = array_merge(self::$migrationCleanupReport, $option);
}
return self::$migrationCleanupReport;
}
/**
* save clean up report in wordpress options
*
* @return boolean
*/
public static function saveCleanupReport()
{
return add_option(self::CLEAN_INSTALL_REPORT_OPTION, self::$migrationCleanupReport, '', 'no');
}
/**
*
* @param array $migrationData Migration data
*
* @return void
*/
public static function removeFirstLoginOption($migrationData)
{
delete_option(self::FIRST_LOGIN_OPTION);
}
/**
*
* @staticvar array $migrationData
*
* @param string|null $key Key to get from migration data
*
* @return mixed
*/
public static function getMigrationData($key = null)
{
static $migrationData = null;
if (is_null($migrationData)) {
$migrationData = get_option(self::MIGRATION_DATA_OPTION, false);
if (is_string($migrationData)) {
$migrationData = json_decode($migrationData, true);
}
}
if (is_null($key)) {
return $migrationData;
} elseif (isset($migrationData[$key])) {
return $migrationData[$key];
} else {
return false;
}
}
/**
*
* @return string
*/
public static function getSaveModeWarning()
{
switch (self::getMigrationData('safeMode')) {
case 1:
//safe_mode basic
return __('NOTICE: Safe mode (Basic) was enabled during install, be sure to re-enable all your plugins.', 'duplicator');
case 2:
//safe_mode advance
return __('NOTICE: Safe mode (Advanced) was enabled during install, be sure to re-enable all your plugins.', 'duplicator');
case 0:
default:
return '';
}
}
/**
* Check the root path and in case there are installer files without hashes rename them.
*
* @param integer $fileTimeDelay If the file is younger than $fileTimeDelay seconds then it is not renamed.
*
* @return void
*/
public static function renameInstallersPhpFiles($fileTimeDelay = 0)
{
$fileTimeDelay = is_numeric($fileTimeDelay) ? (int) $fileTimeDelay : 0;
$pathsTocheck = array(
SnapIO::safePathTrailingslashit(ABSPATH),
SnapIO::safePathTrailingslashit(SnapWP::getHomePath()),
SnapIO::safePathTrailingslashit(WP_CONTENT_DIR)
);
$migrationData = self::getMigrationData();
if (isset($migrationData['installerPath'])) {
$pathsTocheck[] = SnapIO::safePathTrailingslashit(dirname($migrationData['installerPath']));
}
if (isset($migrationData['dupInstallerPath'])) {
$pathsTocheck[] = SnapIO::safePathTrailingslashit(dirname($migrationData['dupInstallerPath']));
}
$pathsTocheck = array_unique($pathsTocheck);
$filesToCheck = array();
foreach ($pathsTocheck as $cFolder) {
if (
!is_dir($cFolder) ||
!is_writable($cFolder) // rename permissions
) {
continue;
}
$cFile = $cFolder . 'installer.php';
if (
!is_file($cFile) ||
!SnapIO::chmod($cFile, 'u+rw') ||
!is_readable($cFile)
) {
continue;
}
$filesToCheck[] = $cFile;
}
$installerTplCheck = '/const\s+ARCHIVE_FILENAME\s*=\s*[\'"](.+?)[\'"]\s*;.*const\s+PACKAGE_HASH\s*=\s*[\'"](.+?)[\'"]\s*;/s';
foreach ($filesToCheck as $file) {
$fileName = basename($file);
if ($fileTimeDelay > 0 && (time() - filemtime($file)) < $fileTimeDelay) {
continue;
}
if (($content = @file_get_contents($file, false, null)) === false) {
continue;
}
$matches = null;
if (preg_match($installerTplCheck, $content, $matches) !== 1) {
continue;
}
$archiveName = $matches[1];
$hash = $matches[2];
$matches = null;
if (preg_match(DUPLICATOR_ARCHIVE_REGEX_PATTERN, $archiveName, $matches) !== 1) {
if (SnapIO::unlink($file)) {
self::$migrationCleanupReport['instFile'][] = "<div class='failure'>"
. "<i class='fa fa-check green'></i> "
. sprintf(__('Installer file <b>%s</b> removed for security reasons', 'duplicator'), esc_html($fileName))
. "</div>";
} else {
self::$migrationCleanupReport['instFile'][] = "<div class='success'>"
. '<i class="fa fa-exclamation-triangle red"></i> '
. sprintf(__('Can\'t remove installer file <b>%s</b>, please remove it for security reasons', 'duplicator'), esc_html($fileName))
. '</div>';
}
continue;
}
$archiveHash = $matches[1];
if (strpos($file, $archiveHash) === false) {
if (SnapIO::rename($file, dirname($file) . '/' . $archiveHash . '_installer.php', true)) {
self::$migrationCleanupReport['instFile'][] = "<div class='failure'>"
. "<i class='fa fa-check green'></i> "
. sprintf(__('Installer file <b>%s</b> renamed with HASH', 'duplicator'), esc_html($fileName))
. "</div>";
} else {
self::$migrationCleanupReport['instFile'][] = "<div class='success'>"
. '<i class="fa fa-exclamation-triangle red"></i> '
. sprintf(
__('Can\'t rename installer file <b>%s</b> with HASH, please remove it for security reasons', 'duplicator'),
esc_html($fileName)
)
. '</div>';
}
}
}
}
/**
*
* @param array $migrationData Migration data
*
* @return void
*/
public static function storeMigrationFiles($migrationData)
{
$ssdInstallerPath = DUP_Settings::getSsdirInstallerPath();
wp_mkdir_p($ssdInstallerPath);
SnapIO::emptyDir($ssdInstallerPath);
SnapIO::createSilenceIndex($ssdInstallerPath);
$filesToMove = array(
$migrationData['installerLog'],
$migrationData['installerBootLog'],
$migrationData['origFileFolderPath']
);
foreach ($filesToMove as $path) {
if (file_exists($path)) {
if (SnapIO::rcopy($path, $ssdInstallerPath . '/' . basename($path), true)) {
self::$migrationCleanupReport['stored'] = "<div class='success'>"
. "<i class='fa fa-check'></i> "
. __('Original files folder moved in installer backup directory', 'duplicator') . " - " . esc_html($path) .
"</div>";
} else {
self::$migrationCleanupReport['stored'] = "<div class='success'>"
. '<i class="fa fa-exclamation-triangle"></i> '
. sprintf(__('Can\'t move %s to %s', 'duplicator'), esc_html($path), $ssdInstallerPath)
. '</div>';
}
}
}
}
/**
*
* @return array
*/
public static function getStoredMigrationLists()
{
if (($migrationData = self::getMigrationData()) == false) {
$filesToCheck = array();
} else {
$filesToCheck = array(
$migrationData['installerLog'] => __('Installer log', 'duplicator'),
$migrationData['installerBootLog'] => __('Installer boot log', 'duplicator'),
$migrationData['origFileFolderPath'] => __('Original files folder', 'duplicator')
);
}
$result = array();
foreach ($filesToCheck as $path => $label) {
$storedPath = DUP_Settings::getSsdirInstallerPath() . '/' . basename($path);
if (!file_exists($storedPath)) {
continue;
}
$result[$storedPath] = $label;
}
return $result;
}
/**
*
* @return bool
*/
public static function haveFileToClean()
{
return count(self::checkInstallerFilesList()) > 0;
}
/**
* Gets a list of all the installer files and directory by name and full path
*
* @remarks
* FILES: installer.php, installer-backup.php, dup-installer-bootlog__[HASH].txt
* DIRS: dup-installer
* Last set is for lazy developer cleanup files that a developer may have
* accidentally left around lets be proactive for the user just in case.
*
* @return [string] // [file_name]
*/
public static function getGenericInstallerFiles()
{
return array(
'installer.php',
'[HASH]installer-backup.php',
'dup-installer',
'dup-installer[HASH]',
'dup-installer-bootlog__[HASH].txt',
'[HASH]_archive.zip|daf'
);
}
/**
*
* @return string[]
* @throws Exception
*/
public static function checkInstallerFilesList()
{
$migrationData = self::getMigrationData();
$foldersToChkeck = array(
SnapIO::safePathTrailingslashit(ABSPATH),
SnapWP::getHomePath(),
);
$result = array();
if (!empty($migrationData)) {
if (
file_exists($migrationData['archivePath']) &&
!DUP_Archive::isBackupPathChild($migrationData['archivePath'])
) {
$result[] = $migrationData['archivePath'];
}
if (
self::isInstallerFile($migrationData['installerPath']) &&
!DUP_Archive::isBackupPathChild($migrationData['archivePath'])
) {
$result[] = $migrationData['installerPath'];
}
if (file_exists($migrationData['installerBootLog'])) {
$result[] = $migrationData['installerBootLog'];
}
if (file_exists($migrationData['dupInstallerPath'])) {
$result[] = $migrationData['dupInstallerPath'];
}
}
foreach ($foldersToChkeck as $folder) {
$result = array_merge($result, SnapIO::regexGlob($folder, array(
'regexFile' => array(
DUPLICATOR_ARCHIVE_REGEX_PATTERN,
DUPLICATOR_INSTALLER_REGEX_PATTERN,
DUPLICATOR_DUP_INSTALLER_BOOTLOG_REGEX_PATTERN,
DUPLICATOR_DUP_INSTALLER_OWRPARAM_REGEX_PATTERN
),
'regexFolder' => array(
DUPLICATOR_DUP_INSTALLER_FOLDER_REGEX_PATTERN
)
)));
}
$result = array_map(array('Duplicator\\Libs\\Snap\\SnapIO', 'safePathUntrailingslashit'), $result);
return array_unique($result);
}
/**
* @param $path string Path to check
*
* @return bool true if the file at current path is the installer file
*/
public static function isInstallerFile($path)
{
if (!is_file($path) || !is_array($last5Lines = SnapIO::getLastLinesOfFile($path, 5)) || empty($last5Lines)) {
return false;
}
return strpos(implode("", $last5Lines), "DUPLICATOR_INSTALLER_EOF") !== false;
}
/**
* Clear all the installer files and directory
*
* @return array
*/
public static function cleanMigrationFiles()
{
$cleanList = self::checkInstallerFilesList();
$result = array();
foreach ($cleanList as $path) {
try {
$success = (SnapIO::rrmdir($path) !== false);
} catch (Exception $ex) {
$success = false;
} catch (Error $ex) {
$success = false;
}
$result[$path] = $success;
}
delete_option(self::CLEAN_INSTALL_REPORT_OPTION);
return $result;
}
}

View File

@@ -0,0 +1,388 @@
<?php
namespace Duplicator\Core\Notifications;
class Notice
{
/**
* Not dismissible.
*
* Constant attended to use as the value of the $args['dismiss'] argument.
* DISMISS_NONE means that the notice is not dismissible.
*/
const DISMISS_NONE = 0;
/**
* Dismissible global.
*
* Constant attended to use as the value of the $args['dismiss'] argument.
* DISMISS_GLOBAL means that the notice will have the dismiss button, and after clicking this button, the notice will be dismissed for all users.
*/
const DISMISS_GLOBAL = 1;
/**
* Dismissible per user.
*
* Constant attended to use as the value of the $args['dismiss'] argument.
* DISMISS_USER means that the notice will have the dismiss button, and after clicking this button, the notice will be dismissed only for the current user..
*/
const DISMISS_USER = 2;
/**
* Constant for notice type info with gray left border
*/
const NOTICE_TYPE_INFO = 'info';
/**
* Constant for notice type warning with yellow left border
*/
const NOTICE_TYPE_WARNING = 'warning';
/**
* Constant for notice type warning with red left border
*/
const NOTICE_TYPE_ERROR = 'error';
/**
* Constant for notice type warning with green left border
*/
const NOTICE_TYPE_SUCCESS = 'success';
/**
* Constant for notice id default prefix
*/
const DEFAULT_PREFIX = 'dup-notice-';
/**
* Constant for addition notice id prefix in case it's a global notice
*/
const GLOBAL_PREFIX = 'global-';
/**
* The wp-options key in which the notices are stored
*/
const DISMISSED_NOTICES_OPTKEY = 'duplicator_dismissed_admin_notices';
/**
* Notices.
*
* @var array
*/
private static $notices = array();
/**
* Init.
*
* @return void
*/
public static function init()
{
static::hooks();
}
/**
* Hooks.
*
* @return void
*/
public static function hooks()
{
add_action('admin_notices', array(__CLASS__, 'display'), PHP_INT_MAX);
add_action('wp_ajax_dup_notice_dismiss', array(__CLASS__, 'dismissAjax'));
}
/**
* Enqueue assets.
*
* @return void
*/
private static function enqueues()
{
wp_enqueue_script(
'dup-admin-notices',
DUPLICATOR_PLUGIN_URL . "assets/js/notifications/notices.js",
array('jquery'),
DUPLICATOR_VERSION,
true
);
wp_localize_script(
'dup-admin-notices',
'dup_admin_notices',
array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('duplicator-admin-notice'),
)
);
}
/**
* Display the notices.
*
* @return void
*/
public static function display()
{
$dismissed_notices = get_user_meta(get_current_user_id(), self::DISMISSED_NOTICES_OPTKEY, true);
$dismissed_notices = is_array($dismissed_notices) ? $dismissed_notices : array();
$dismissed_notices = array_merge($dismissed_notices, (array)get_option(self::DISMISSED_NOTICES_OPTKEY, array()));
foreach (self::$notices as $slug => $notice) {
if (isset($dismissed_notices[$slug])) {
unset(self::$notices[$slug]);
}
}
$output = implode('', self::$notices);
echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
// Enqueue script only when it's needed.
if (strpos($output, 'is-dismissible') !== false) {
self::enqueues();
}
}
/**
* Add notice to the registry.
*
* @param string $message Message to display.
* @param string $slug Unique slug identifying the notice
* @param string $type Type of the notice. Can be [ '' (default) | 'info' | 'error' | 'success' | 'warning' ].
* @param array $args The array of additional arguments. Please see the $defaults array below.
*
* @return void
*/
public static function add($message, $slug, $type = '', $args = array())
{
$defaults = array(
'dismiss' => self::DISMISS_NONE, // Dismissible level: one of the self::DISMISS_* const. By default notice is not dismissible.
'autop' => true, // `false` if not needed to pass message through wpautop().
'class' => '' // Additional CSS class.
);
$args = wp_parse_args($args, $defaults);
$dismiss = (int)$args['dismiss'];
$classes = array();
if (!empty($type)) {
$classes[] = 'notice-' . esc_attr(sanitize_key($type));
}
if (!empty($args['class'])) {
$classes[] = esc_attr(sanitize_key($args['class']));
}
if ($dismiss > self::DISMISS_NONE) {
$classes[] = 'is-dismissible';
}
$id = $dismiss === self::DISMISS_GLOBAL ? self::DEFAULT_PREFIX . self::GLOBAL_PREFIX . $slug : self::DEFAULT_PREFIX . $slug;
$message = $args['autop'] ? wpautop($message) : $message;
$notice = sprintf(
'<div class="notice dup-notice %s" id="%s">%s</div>',
esc_attr(implode(' ', $classes)),
esc_attr($id),
$message
);
self::$notices[$slug] = $notice;
}
/**
* Add multistep notice.
*
* @param array $steps Array of info for each step.
* @param string $slug Unique slug identifying the notice
* @param string $type Type of the notice. Can be [ '' (default) | 'info' | 'error' | 'success' | 'warning' ].
* @param array $args Array of additional arguments. Details in the self::add() method.
*
* @return void
*/
public static function addMultistep($steps, $slug, $type = '', $args = array())
{
$content = '<div class="dup-multi-notice">';
foreach ($steps as $i => $step) {
$hide = $i === 0 ? '' : ' style="display: none;"';
$content .= '<div class="dup-multi-notice-step dup-multi-notice-step-' . $i . '"' . $hide . '>';
$content .= $step['message'];
$content .= "<p>";
foreach ($step["links"] as $link) {
$url = isset($link['url']) ? $link['url'] : "#";
$target = isset($link['url']) ? 'target="_blank"' : '';
$switch = isset($link['switch']) ? ' data-step="' . $link['switch'] . '"' : '';
$dismiss = isset($link['dismiss']) && $link['dismiss'] ? ' class="dup-notice-dismiss"' : '';
$content .= '<a href="' . $url . '"' . $dismiss . $switch . $target . '>' . $link['text'] . '</a><br>';
}
$content .= "</p>";
$content .= "</div>";
}
$content .= "</div>";
self::add($content, $slug, $type, $args);
}
/**
* Add info notice.
*
* @param string $message Message to display.
* @param string $slug Unique slug identifying the notice
* @param array $args Array of additional arguments. Details in the self::add() method.
*
* @return void
*/
public static function info($message, $slug, $args = array())
{
self::add($message, $slug, self::NOTICE_TYPE_INFO, $args);
}
/**
* Add error notice.
*
* @param string $message Message to display.
* @param string $slug Unique slug identifying the notice
* @param array $args Array of additional arguments. Details in the self::add() method.
*
* @return void
*/
public static function error($message, $slug, $args = array())
{
self::add($message, $slug, self::NOTICE_TYPE_ERROR, $args);
}
/**
* Add success notice.
*
* @param string $message Message to display.
* @param string $slug Unique slug identifying the notice
* @param array $args Array of additional arguments. Details in the self::add() method.
*
* @return void
*/
public static function success($message, $slug, $args = array())
{
self::add($message, $slug, self::NOTICE_TYPE_SUCCESS, $args);
}
/**
* Add warning notice.
*
* @param string $message Message to display.
* @param string $slug Unique slug identifying the notice
* @param array $args Array of additional arguments. Details in the self::add() method.
*
* @return void
*/
public static function warning($message, $slug, $args = array())
{
self::add($message, $slug, self::NOTICE_TYPE_WARNING, $args);
}
/**
* AJAX routine that updates dismissed notices meta data.
*
* @return void
*/
public static function dismissAjax()
{
// Run a security check.
check_ajax_referer('duplicator-admin-notice', 'nonce');
// Sanitize POST data.
$post = array_map('sanitize_key', wp_unslash($_POST));
// Update notices meta data.
if (strpos($post['id'], self::GLOBAL_PREFIX) !== false) {
// Check for permissions.
if (!current_user_can('manage_options')) {
wp_send_json_error();
}
$notices = self::dismissGlobal($post['id']);
$level = self::DISMISS_GLOBAL;
} else {
$notices = self::dismissUser($post['id']);
$level = self::DISMISS_USER;
}
/**
* Allows developers to apply additional logic to the dismissing notice process.
* Executes after updating option or user meta (according to the notice level).
*
* @param string $notice_id Notice ID (slug).
* @param integer $level Notice level.
* @param array $notices Dismissed notices.
*/
do_action('duplicator_admin_notice_dismiss_ajax', $post['id'], $level, $notices);
wp_send_json_success(
array(
'id' => $post['id'],
'time' => time(),
'level' => $level,
'notices' => $notices
)
);
}
/**
* AJAX sub-routine that updates dismissed notices option.
*
* @param string $id Notice Id.
*
* @return array Notices.
*/
private static function dismissGlobal($id)
{
$id = str_replace(self::GLOBAL_PREFIX, '', $id);
$notices = get_option(self::DISMISSED_NOTICES_OPTKEY, array());
$notices[$id] = array(
'time' => time()
);
update_option(self::DISMISSED_NOTICES_OPTKEY, $notices, true);
return $notices;
}
/**
* AJAX sub-routine that updates dismissed notices user meta.
*
* @param string $id Notice Id.
*
* @return array Notices.
*/
private static function dismissUser($id)
{
$user_id = get_current_user_id();
$notices = get_user_meta($user_id, self::DISMISSED_NOTICES_OPTKEY, true);
$notices = !is_array($notices) ? array() : $notices;
$notices[$id] = array(
'time' => time()
);
update_user_meta($user_id, self::DISMISSED_NOTICES_OPTKEY, $notices);
return $notices;
}
/**
* Delete related option
*
* @return void
*/
public static function deleteOption()
{
delete_option(self::DISMISSED_NOTICES_OPTKEY);
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace Duplicator\Core\Notifications;
use Duplicator\Core\Controllers\ControllersManager;
use Duplicator\Core\Views\TplMng;
use Duplicator\Libs\Snap\SnapWP;
/**
* Admin/NoticeBar Education feature for Lite.
*/
class NoticeBar
{
/**
* Constant for the wp-options key handling the dismissed state
*/
const NOTICE_BAR_DISMISSED_OPT_KEY = 'duplicator_notice_bar_dismissed';
/**
* Init.
*
* @return void
*/
public static function init()
{
add_action('in_admin_header', array(__CLASS__, 'display'));
add_action('wp_ajax_duplicator_notice_bar_dismiss', array(__CLASS__, 'dismissNoticeBar'));
}
/**
* Notice bar display message.
*
* @return void
*/
public static function display()
{
if (!ControllersManager::isDuplicatorPage()) {
return;
}
//make sure it wasn't dismissed
if (get_user_meta(get_current_user_id(), self::NOTICE_BAR_DISMISSED_OPT_KEY, true) != false) {
return;
}
$utm_content = '';
foreach (ControllersManager::getMenuLevels() as $key => $value) {
if (strlen((string) $value) == 0) {
continue;
}
$utm_content .= ucfirst($key) . ' ' . $value . ' ';
}
$utm_content = trim($utm_content);
TplMng::getInstance()->render('/parts/notice-bar', array(
'utm_content' => $utm_content
));
}
/**
* Dismiss notice bar ajax action
*
* @return void
*/
public static function dismissNoticeBar()
{
// Run a security check.
check_ajax_referer('duplicator-notice-bar-dismiss', 'nonce');
update_user_meta(get_current_user_id(), self::NOTICE_BAR_DISMISSED_OPT_KEY, true);
}
/**
* Delete related option
*
* @return bool true on success, false on failure
*/
public static function deleteOption()
{
return SnapWP::deleteUserMetaKey(self::NOTICE_BAR_DISMISSED_OPT_KEY);
}
}

View File

@@ -0,0 +1,613 @@
<?php
namespace Duplicator\Core\Notifications;
use DUP_LITE_Plugin_Upgrade;
use DUP_Settings;
use Duplicator\Ajax\ServicesNotifications;
use Duplicator\Core\Views\TplMng;
/**
* Notifications.
*/
class Notifications
{
/**
* Source of notifications content.
*
* @var string
*/
const SOURCE_URL = 'https://notifications.duplicator.com/dp-notifications.json';
/**
* WordPress option key containing notification data
*
* @var string
*/
const DUPLICATOR_NOTIFICATIONS_OPT_KEY = 'duplicator_notifications';
/**
* WordPress option key containing notification data
*
* @var string
*/
const DUPLICATOR_BEFORE_PACKAGES_HOOK = 'duplicator_before_packages_table_action';
/**
* Duplicator notifications dismiss nonce key
*
* @var string
*/
const DUPLICATOR_NOTIFICATION_NONCE_KEY = 'duplicator-notification-dismiss';
/**
* Option value.
*
* @var bool|array
*/
public static $option = false;
/**
* Initialize class.
*
* @return void
*/
public static function init()
{
// Delete data even if notifications are disabled.
add_action('deactivate_plugin', array(__CLASS__, 'delete'), 10, 2);
if (!DUP_Settings::Get('amNotices')) {
return;
}
// Add notification count to menu label.
add_filter('duplicator_menu_label_duplicator', function ($label) {
if (self::getCount() === 0) {
return $label;
}
return $label . '<span class="awaiting-mod">' . self::getCount() . '</span>';
});
self::update();
add_action(self::DUPLICATOR_BEFORE_PACKAGES_HOOK, array(__CLASS__, 'output'));
$notificationsService = new ServicesNotifications();
$notificationsService->init();
}
/**
* Check if user has access and is enabled.
*
* @return bool
*/
public static function hasAccess()
{
return current_user_can('manage_options');
}
/**
* Get option value.
*
* @param bool $cache Reference property cache if available.
*
* @return array
*/
public static function getOption($cache = true)
{
if (self::$option && $cache) {
return self::$option;
}
$option = get_option(self::DUPLICATOR_NOTIFICATIONS_OPT_KEY, array());
self::$option = array(
'update' => !empty($option['update']) ? (int)$option['update'] : 0,
'feed' => !empty($option['feed']) ? (array)$option['feed'] : array(),
'events' => !empty($option['events']) ? (array)$option['events'] : array(),
'dismissed' => !empty($option['dismissed']) ? (array)$option['dismissed'] : array()
);
return self::$option;
}
/**
* Fetch notifications from feed.
*
* @return array
*/
public static function fetchFeed()
{
$response = wp_remote_get(
self::SOURCE_URL,
array(
'timeout' => 10,
'user-agent' => self::getUserAgent(),
)
);
if (is_wp_error($response)) {
return array();
}
$body = wp_remote_retrieve_body($response);
if (empty($body)) {
return array();
}
return self::verify(json_decode($body, true));
}
/**
* Verify notification data before it is saved.
*
* @param array $notifications Array of notifications items to verify.
*
* @return array
*/
public static function verify($notifications)
{
$data = array();
if (!is_array($notifications) || empty($notifications)) {
return $data;
}
foreach ($notifications as $notification) {
// Ignore if one of the conditional checks is true:
//
// 1. notification message is empty.
// 2. license type does not match.
// 3. notification is expired.
// 4. notification has already been dismissed.
// 5. notification existed before installing Duplicator.
// (Prevents bombarding the user with notifications after activation).
if (
empty($notification['content']) ||
!self::isLicenseTypeMatch($notification) ||
self::isExpired($notification) ||
self::isDismissed($notification) ||
self::isExisted($notification)
) {
continue;
}
$data[] = $notification;
}
return $data;
}
/**
* Verify saved notification data for active notifications.
*
* @param array $notifications Array of notifications items to verify.
*
* @return array
*/
public static function verifyActive($notifications)
{
if (!is_array($notifications) || empty($notifications)) {
return array();
}
$current_timestamp = time();
// Remove notifications that are not active.
foreach ($notifications as $key => $notification) {
if (
(!empty($notification['start']) && $current_timestamp < strtotime($notification['start'])) ||
(!empty($notification['end']) && $current_timestamp > strtotime($notification['end']))
) {
unset($notifications[$key]);
}
}
return $notifications;
}
/**
* Get notification data.
*
* @return array
*/
public static function get()
{
if (!self::hasAccess()) {
return array();
}
$option = self::getOption();
$feed = !empty($option['feed']) ? self::verifyActive($option['feed']) : array();
$events = !empty($option['events']) ? self::verifyActive($option['events']) : array();
return array_merge($feed, $events);
}
/**
* Get notification count.
*
* @return int
*/
public static function getCount()
{
return count(self::get());
}
/**
* Add a new Event Driven notification.
*
* @param array $notification Notification data.
*
* @return void
*/
public static function add($notification)
{
if (!self::isValid($notification)) {
return;
}
$option = self::getOption();
// Notification ID already exists.
if (!empty($option['events'][$notification['id']])) {
return;
}
$notification = self::verify(array($notification));
update_option(
self::DUPLICATOR_NOTIFICATIONS_OPT_KEY,
array(
'update' => $option['update'],
'feed' => $option['feed'],
'events' => array_merge($notification, $option['events']),
'dismissed' => $option['dismissed'],
)
);
}
/**
* Determine if notification data is valid.
*
* @param array $notification Notification data.
*
* @return bool
*/
public static function isValid($notification)
{
if (empty($notification['id'])) {
return false;
}
return count(self::verify(array($notification))) > 0;
}
/**
* Determine if notification has already been dismissed.
*
* @param array $notification Notification data.
*
* @return bool
*/
private static function isDismissed($notification)
{
$option = self::getOption();
return !empty($option['dismissed']) && in_array($notification['id'], $option['dismissed']);
}
/**
* Determine if license type is match.
*
* @param array $notification Notification data.
*
* @return bool
*/
private static function isLicenseTypeMatch($notification)
{
// A specific license type is not required.
$notification['type'] = (array)$notification['type'];
if (empty($notification['type'])) {
return false;
}
if (in_array('any', $notification['type'])) {
return true;
}
return in_array(self::getLicenseType(), (array)$notification['type'], true);
}
/**
* Determine if notification is expired.
*
* @param array $notification Notification data.
*
* @return bool
*/
private static function isExpired($notification)
{
return !empty($notification['end']) && time() > strtotime($notification['end']);
}
/**
* Determine if notification existed before installing Duplicator.
*
* @param array $notification Notification data.
*
* @return bool
*/
private static function isExisted($notification)
{
$installInfo = DUP_LITE_Plugin_Upgrade::getInstallInfo();
return (!empty($notification['start']) && $installInfo['time'] > strtotime($notification['start']));
}
/**
* Update notification data from feed.
*
* @return void
*/
public static function update()
{
$option = self::getOption();
//Only update twice daily
if ($option['update'] !== 0 && time() < $option['update'] + DAY_IN_SECONDS / 2) {
return;
}
$data = array(
'update' => time(),
'feed' => self::fetchFeed(),
'events' => $option['events'],
'dismissed' => $option['dismissed'],
);
/**
* Allow changing notification data before it will be updated in database.
*
* @param array $data New notification data.
*/
$data = (array)apply_filters('duplicator_admin_notifications_update_data', $data);
update_option(self::DUPLICATOR_NOTIFICATIONS_OPT_KEY, $data);
}
/**
* Remove notification data from database before a plugin is deactivated.
*
* @param string $plugin Path to the plugin file relative to the plugins directory.
*
* @return void
*/
public static function delete($plugin)
{
$duplicator_plugins = array(
'duplicator-lite/duplicator.php',
'duplicator/duplicator.php',
);
if (!in_array($plugin, $duplicator_plugins, true)) {
return;
}
delete_option(self::DUPLICATOR_NOTIFICATIONS_OPT_KEY);
}
/**
* Enqueue assets on Form Overview admin page.
*
* @return void
*/
public static function enqueues()
{
if (!self::getCount()) {
return;
}
wp_enqueue_style(
'dup-admin-notifications',
DUPLICATOR_PLUGIN_URL . "assets/css/admin-notifications.css",
array('dup-lity'),
DUPLICATOR_VERSION
);
wp_enqueue_script(
'dup-admin-notifications',
DUPLICATOR_PLUGIN_URL . "assets/js/notifications/admin-notifications.js",
array('jquery', 'dup-lity'),
DUPLICATOR_VERSION,
true
);
wp_localize_script(
'dup-admin-notifications',
'dup_admin_notifications',
array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce(self::DUPLICATOR_NOTIFICATION_NONCE_KEY),
)
);
// Lity.
wp_enqueue_style(
'dup-lity',
DUPLICATOR_PLUGIN_URL . 'assets/lib/lity/lity.min.css',
array(),
DUPLICATOR_VERSION
);
wp_enqueue_script(
'dup-lity',
DUPLICATOR_PLUGIN_URL . 'assets/lib/lity/lity.min.js',
array('jquery'),
DUPLICATOR_VERSION,
true
);
}
/**
* Output notifications on Form Overview admin area.
*
* @return void
*/
public static function output()
{
$notificationsData = self::get();
if (empty($notificationsData)) {
return;
}
$content_allowed_tags = array(
'br' => array(),
'em' => array(),
'strong' => array(),
'span' => array(
'style' => array()
),
'p' => array(
'id' => array(),
'class' => array()
),
'a' => array(
'href' => array(),
'target' => array(),
'rel' => array()
)
);
$notifications = array();
foreach ($notificationsData as $notificationData) {
// Prepare required arguments.
$notificationData = wp_parse_args(
$notificationData,
array(
'id' => 0,
'title' => '',
'content' => '',
'video' => ''
)
);
$title = self::getComponentData($notificationData['title']);
$content = self::getComponentData($notificationData['content']);
if (!$title && !$content) {
continue;
}
$notifications[] = array(
'id' => $notificationData['id'],
'title' => $title,
'btns' => self::getButtonsData($notificationData),
'content' => wp_kses(wpautop($content), $content_allowed_tags),
'video_url' => wp_http_validate_url(self::getComponentData($notificationData['video'])),
);
}
self::enqueues();
TplMng::getInstance()->render(
'parts/Notifications/main',
array(
'notifications' => $notifications
)
);
}
/**
* Retrieve notification's buttons.
*
* @param array $notification Notification data.
*
* @return array
*/
private static function getButtonsData($notification)
{
if (empty($notification['btn']) || !is_array($notification['btn'])) {
return array();
}
$buttons = array();
if (!empty($notification['btn']['main_text']) && !empty($notification['btn']['main_url'])) {
$buttons[] = array(
'type' => 'primary',
'text' => $notification['btn']['main_text'],
'url' => self::prepareBtnUrl($notification['btn']['main_url']),
'target' => '_blank'
);
}
if (!empty($notification['btn']['alt_text']) && !empty($notification['btn']['alt_url'])) {
$buttons[] = array(
'type' => 'secondary',
'text' => $notification['btn']['alt_text'],
'url' => self::prepareBtnUrl($notification['btn']['alt_url']),
'target' => '_blank'
);
}
return $buttons;
}
/**
* Retrieve notification's component data by a license type.
*
* @param mixed $data Component data.
*
* @return false|mixed
*/
private static function getComponentData($data)
{
if (empty($data['license'])) {
return $data;
}
$license_type = self::getLicenseType();
return !empty($data['license'][$license_type]) ? $data['license'][$license_type] : false;
}
/**
* Retrieve the current installation license type (always lowercase).
*
* @return string
*/
private static function getLicenseType()
{
return 'lite';
}
/**
* Prepare button URL.
*
* @param string $btnUrl Button url.
*
* @return string
*/
private static function prepareBtnUrl($btnUrl)
{
if (empty($btnUrl)) {
return '';
}
$replace_tags = array(
'{admin_url}' => admin_url()
);
return wp_http_validate_url(str_replace(array_keys($replace_tags), array_values($replace_tags), $btnUrl));
}
/**
* User agent that will be used for the request
*
* @return string
*/
private static function getUserAgent()
{
return 'WordPress/' . get_bloginfo('version') . '; ' . get_bloginfo('url') . '; Duplicator/Lite-' . DUPLICATOR_VERSION;
}
}

View File

@@ -0,0 +1,240 @@
<?php
namespace Duplicator\Core\Notifications;
use DUP_LITE_Plugin_Upgrade;
use DUP_Package;
use Duplicator\Core\MigrationMng;
use Duplicator\Core\Controllers\ControllersManager;
/**
* Ask for some love.
*/
class Review
{
/**
* Constant for the review request admin notice slug
*/
const REVIEW_REQUEST_NOTICE_SLUG = 'review_request';
/**
* Primary class constructor.
*
* @return void
*/
public static function init()
{
// Admin notice requesting review.
add_action('admin_init', array(__CLASS__, 'reviewRequest'));
// Admin footer text.
add_filter('admin_footer_text', array(__CLASS__, 'adminFooter'), 1, 2);
// Admin footer version text.
add_filter('update_footer', array(__CLASS__, 'adminFooterVersion'), 9999);
}
/**
* Add admin notices as needed for reviews.
*
* @return void
*/
public static function reviewRequest()
{
// Only consider showing the review request to admin users.
if (!is_super_admin()) {
return;
}
// Get dismissed notices.
$notices = get_option(Notice::DISMISSED_NOTICES_OPTKEY, array());
//has already been dismissed, don't show again
if (isset($notices[self::REVIEW_REQUEST_NOTICE_SLUG])) {
return;
}
self::reviewLite();
}
/**
* Maybe show Lite review request.
*
* @return void
*/
public static function reviewLite()
{
$display = false;
// Fetch when plugin was initially installed.
$installInfo = DUP_LITE_Plugin_Upgrade::getInstallInfo();
$numberOfPackages = DUP_Package::count_by_status(array(
array('op' => '=' , 'status' => \DUP_PackageStatus::COMPLETE )
));
// Display if plugin has been installed for at least 3 days and has a package installed
if ((($installInfo['time'] + (DAY_IN_SECONDS * 3)) < time() && $numberOfPackages > 0)) {
$display = true;
}
//Display if it's been 3 days after a successful migration
$migrationTime = MigrationMng::getMigrationData('time');
if (!$display && $migrationTime !== false && (($migrationTime + (DAY_IN_SECONDS * 3)) < time())) {
$display = true;
}
if (!$display) {
return;
}
Notice::addMultistep(
array(
array(
"message" => "<p>" . sprintf(__('Are you enjoying %s?', 'duplicator'), 'Duplicator') . "</p>",
"links" => array(
array(
"text" => __('Yes', 'duplicator'),
"switch" => 1
),
array(
"text" => __('Not really', 'duplicator'),
"switch" => 2
)
)
),
array(
"message" => "<p>" .
__(
'Thats awesome! Could you please do me a BIG favor and give it a 5-star rating on ' .
'WordPress to help us spread the word and boost our motivation?',
'duplicator'
) . "</p>" .
"<p>" . wp_kses(__('~ John Turner<br>President of Duplicator', 'duplicator'), array('br' => array())) . "</p>",
"links" => array(
array(
"url" => self::getReviewUrl(),
"text" => __('Ok, you deserve it', 'duplicator'),
"dismiss" => true
),
array(
"text" => __('Nope, maybe later', 'duplicator'),
"dismiss" => true
),
array(
"text" => __('I already did', 'duplicator'),
"dismiss" => true
)
)
),
array(
"message" => "<p>" .
__(
'We\'re sorry to hear you aren\'t enjoying Duplicator. We would love a chance to improve. ' .
'Could you take a minute and let us know what we can do better?',
'duplicator'
) . "</p>",
"links" => array(
array(
"url" => self::getFeedbackUrl(),
"text" => __('Give Feedback', 'duplicator'),
"dismiss" => true
),
array(
"text" => __('No thanks', 'duplicator'),
"dismiss" => true
)
)
)
),
self::REVIEW_REQUEST_NOTICE_SLUG,
Notice::NOTICE_TYPE_INFO,
array(
'dismiss' => Notice::DISMISS_GLOBAL,
'autop' => false,
'class' => 'dup-review-notice',
)
);
}
/**
* @return string The review url on wordpress.org
*/
public static function getReviewUrl()
{
return "https://wordpress.org/support/plugin/duplicator/reviews/#new-post";
}
/**
* @return string The snapcreek feedback url
*/
public static function getFeedbackUrl()
{
return DUPLICATOR_BLOG_URL . "contact/";
}
/**
* Updates admin footer text by adding Duplicator version
*
* @param string $defaultText Default WP footer text
*
* @return string Modified version text
*/
public static function adminFooterVersion($defaultText)
{
if (!ControllersManager::isDuplicatorPage()) {
return $defaultText;
}
$defaultText = sprintf(
'%1$s | Duplicator %2$s',
$defaultText,
esc_html(DUPLICATOR_VERSION)
);
return $defaultText;
}
/**
* When user is on a Duplicator related admin page, display footer text
* that graciously asks them to rate us.
*
* @param string $text Footer text.
*
* @return string
*/
public static function adminFooter($text)
{
//Show only on duplicator pages
if (
! is_admin() ||
empty($_REQUEST['page']) ||
strpos($_REQUEST['page'], 'duplicator') === false
) {
return false;
}
$text = sprintf(
wp_kses( /* translators: $1$s - WPForms plugin name; $2$s - WP.org review link; $3$s - WP.org review link. */
__(
'Please rate <strong>Duplicator</strong> ' .
'<a href="%1$s" target="_blank" rel="noopener noreferrer">&#9733;&#9733;&#9733;&#9733;&#9733;</a>' .
' on <a href="%1$s" target="_blank" rel="noopener">WordPress.org</a> to help us spread the word. Thank you from the Duplicator team!',
'duplicator'
),
array(
'a' => array(
'href' => array(),
'target' => array(),
'rel' => array(),
),
'strong' => array()
)
),
self::getReviewUrl()
);
return $text;
}
}

View File

@@ -0,0 +1,42 @@
<?php
/**
* Interface that collects the functions of initial duplicator Bootstrap
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Core;
/**
* Uninstall class
*/
class Unistall
{
/**
* Registrer unistall hoosk
*
* @return void
*/
public static function registerHooks()
{
if (is_admin()) {
register_deactivation_hook(DUPLICATOR_LITE_FILE, array(__CLASS__, 'deactivate'));
}
}
/**
* Deactivation Hook:
* Hooked into `register_deactivation_hook`. Routines used to deactivate the plugin
* For uninstall see uninstall.php WordPress by default will call the uninstall.php file
*
* @return void
*/
public static function deactivate()
{
MigrationMng::renameInstallersPhpFiles();
do_action('duplicator_after_deactivation');
}
}

View File

@@ -0,0 +1,148 @@
<?php
/**
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Core\Upgrade;
use DUP_Settings;
use Duplicator\Utils\Email\EmailSummary;
use Duplicator\Libs\Snap\SnapIO;
use DUP_Log;
use Exception;
/**
* Utility class managing actions when the plugin is updated
*/
class UpgradeFunctions
{
const LAST_VERSION_EMAIL_SUMMARY_WRONG_KEY = '1.5.6.1';
const FIRST_VERSION_NEW_STORAGE_POSITION = '1.3.35';
const FIRST_VERSION_FOLDER_MIGRATION = '1.5.14';
const FIRST_VERSION_WITH_LOGS_SUBFOLDER = '1.5.14-beta1';
/**
* This function is executed when the plugin is activated and
* every time the version saved in the wp_options is different from the plugin version both in upgrade and downgrade.
*
* @param false|string $currentVersion current Duplicator version, false if is first installation
* @param string $newVersion new Duplicator Version
*
* @return void
*/
public static function performUpgrade($currentVersion, $newVersion): void
{
self::updateStoragePostition($currentVersion);
self::emailSummaryOptKeyUpdate($currentVersion);
self::migrateStorageFolders($currentVersion, $newVersion);
self::migrateLogsToSubfolder($currentVersion);
}
/**
* Update email summary option key seperator from '-' to '_'
*
* @param false|string $currentVersion current Duplicator version, false if is first installation
*
* @return void
*/
private static function emailSummaryOptKeyUpdate($currentVersion): void
{
if ($currentVersion == false || version_compare($currentVersion, self::LAST_VERSION_EMAIL_SUMMARY_WRONG_KEY, '>')) {
return;
}
if (($data = get_option(EmailSummary::INFO_OPT_OLD_KEY)) !== false) {
update_option(EmailSummary::INFO_OPT_KEY, $data);
delete_option(EmailSummary::INFO_OPT_OLD_KEY);
}
}
/**
* Update storage position option
*
* @param false|string $currentVersion current Duplicator version, false if is first installation
*
* @return void
*/
private static function updateStoragePostition($currentVersion): void
{
//PRE 1.3.35
//Do not update to new wp-content storage till after
if ($currentVersion !== false && version_compare($currentVersion, self::FIRST_VERSION_NEW_STORAGE_POSITION, '<')) {
DUP_Settings::Set('storage_position', DUP_Settings::STORAGE_POSITION_LEGACY);
}
}
/**
* Migrate storage folders from legacy to new location
*
* @param false|string $currentVersion current Duplicator version, false if first install
*
* @return void
*/
private static function migrateStorageFolders($currentVersion): void
{
// Skip on fresh installs or if already past migration version
if ($currentVersion === false || version_compare($currentVersion, self::FIRST_VERSION_FOLDER_MIGRATION, '>=')) {
return;
}
// If storage position is already set to new, do not migrate
if (DUP_Settings::Get('storage_position') === DUP_Settings::STORAGE_POSITION_WP_CONTENT) {
return;
}
// Force using wp-content storage position
DUP_Settings::setStoragePosition(DUP_Settings::STORAGE_POSITION_WP_CONTENT);
DUP_Settings::Save();
}
/**
* Migrate existing log files from root directory to logs subfolder
*
* @param false|string $currentVersion current Duplicator version, false if is first installation
*
* @return void
*/
private static function migrateLogsToSubfolder($currentVersion): void
{
if ($currentVersion === false || version_compare($currentVersion, self::FIRST_VERSION_WITH_LOGS_SUBFOLDER, '>=')) {
return;
}
try {
DUP_Log::Trace("MIGRATION: Moving log files to logs subfolder");
// Ensure logs directory exists
if (!file_exists(DUP_Settings::getSsdirLogsPath())) {
SnapIO::dirWriteCheckOrMkdir(DUP_Settings::getSsdirLogsPath(), 'u+rwx');
}
// Use SnapIO::regexGlob for more robust file discovery
$logFiles = SnapIO::regexGlob(DUP_Settings::getSsdirPath(), [
'regexFile' => [
'/.*\.log$/',
'/.*\.log1$/',
],
'regexFolder' => false,
'recursive' => false,
]);
$migratedCount = 0;
foreach ($logFiles as $oldPath) {
$filename = basename($oldPath);
$newPath = DUP_Settings::getSsdirLogsPath() . '/' . $filename;
if (SnapIO::rename($oldPath, $newPath)) {
$migratedCount++;
}
}
DUP_Log::Trace("MIGRATION: Moved {$migratedCount} log files to logs subfolder - old location is now clean");
} catch (Exception $e) {
DUP_Log::Trace("MIGRATION: Error moving log files: " . $e->getMessage());
}
}
}

View File

@@ -0,0 +1,244 @@
<?php
/**
* Template view manager
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Core\Views;
use Duplicator\Libs\Snap\SnapJson;
final class TplMng
{
/** @var ?self */
private static $instance = null;
/** @var string */
private $mainFolder = '';
/** @var bool */
private static $stripSpaces = false;
/** @var mixed[] */
private $globalData = array();
/**
*
* @return self
*/
public static function getInstance()
{
if (is_null(self::$instance)) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Class constructor
*/
private function __construct()
{
$this->mainFolder = DUPLICATOR_PLUGIN_PATH . '/template/';
}
/**
* If strip spaces is true in render method spaced between tag are removed
*
* @param bool $strip if true strip spaces
*
* @return void
*/
public static function setStripSpaces($strip)
{
self::$stripSpaces = (bool) $strip;
}
/**
* Set template global value in template data
*
* @param string $key global value key
* @param mixed $val value
*
* @return void
*/
public function setGlobalValue($key, $val)
{
$this->globalData[$key] = $val;
}
/**
* Remove global value if exist
*
* @param string $key gloval value key
*
* @return void
*/
public function unsetGlobalValue($key)
{
if (isset($this->globalData[$key])) {
unset($this->globalData[$key]);
}
}
/**
* Return true if global values exists
*
* @param string $key gloval value key
*
* @return bool
*/
public function hasGlobalValue($key)
{
return isset($this->globalData[$key]);
}
/**
* Multiple global data set
*
* @param array<string, mixed> $data data tu set in global data
*
* @return void
*/
public function updateGlobalData(array $data = array())
{
$this->globalData = array_merge($this->globalData, (array) $data);
}
/**
* Return global data
*
* @return array<string, mixed>
*/
public function getGlobalData()
{
return $this->globalData;
}
/**
* Render template
*
* @param string $slugTpl template file is a relative path from root template folder
* @param array<string, mixed> $args array key / val where key is the var name in template
* @param bool $echo if false return template in string
*
* @return string
*/
public function render($slugTpl, $args = array(), $echo = true)
{
ob_start();
if (($renderFile = $this->getFileTemplate($slugTpl)) !== false) {
$tplData = apply_filters(self::getDataHook($slugTpl), array_merge($this->globalData, $args));
$tplMng = $this;
require($renderFile);
} else {
echo '<p>FILE TPL NOT FOUND: ' . $slugTpl . '</p>';
}
$renderResult = apply_filters(self::getRenderHook($slugTpl), ob_get_clean());
if (self::$stripSpaces) {
$renderResult = preg_replace('~>[\n\s]+<~', '><', $renderResult);
}
if ($echo) {
echo $renderResult;
return '';
} else {
return $renderResult;
}
}
/**
* Render template in json string
*
* @param string $slugTpl template file is a relative path from root template folder
* @param array<string, mixed> $args array key / val where key is the var name in template
* @param bool $echo if false return template in string
*
* @return string
*/
public function renderJson($slugTpl, $args = array(), $echo = true)
{
$renderResult = SnapJson::jsonEncode($this->render($slugTpl, $args, false));
if ($echo) {
echo $renderResult;
return '';
} else {
return $renderResult;
}
}
/**
* Render template apply esc attr
*
* @param string $slugTpl template file is a relative path from root template folder
* @param array<string, mixed> $args array key / val where key is the var name in template
* @param bool $echo if false return template in string
*
* @return string
*/
public function renderEscAttr($slugTpl, $args = array(), $echo = true)
{
$renderResult = esc_attr($this->render($slugTpl, $args, false));
if ($echo) {
echo $renderResult;
return '';
} else {
return $renderResult;
}
}
/**
* Get hook unique from template slug
*
* @param string $slugTpl template slug
*
* @return string
*/
public static function tplFileToHookSlug($slugTpl)
{
return str_replace(array('\\', '/', '.'), '_', $slugTpl);
}
/**
* Return data hook from template slug
*
* @param string $slugTpl template slug
*
* @return string
*/
public static function getDataHook($slugTpl)
{
return 'duplicator_template_data_' . self::tplFileToHookSlug($slugTpl);
}
/**
* Return render hook from template slug
*
* @param string $slugTpl template slug
*
* @return string
*/
public static function getRenderHook($slugTpl)
{
return 'duplicator_template_render_' . self::tplFileToHookSlug($slugTpl);
}
/**
* Acctept html of php extensions. if the file have unknown extension automatic add the php extension
*
* @param string $slugTpl template slug
*
* @return boolean|string return false if don\'t find the template file
*/
protected function getFileTemplate($slugTpl)
{
$fullPath = $this->mainFolder . $slugTpl . '.php';
if (file_exists($fullPath)) {
return $fullPath;
} else {
return false;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,286 @@
<?php
namespace Duplicator\Libs\DupArchive;
use Duplicator\Libs\DupArchive\Headers\DupArchiveReaderDirectoryHeader;
use Duplicator\Libs\DupArchive\Headers\DupArchiveReaderFileHeader;
use Duplicator\Libs\DupArchive\Headers\DupArchiveReaderGlobHeader;
use Duplicator\Libs\DupArchive\Headers\DupArchiveReaderHeader;
use Error;
use Exception;
class DupArchive
{
const DUPARCHIVE_VERSION = '1.0.0';
const INDEX_FILE_NAME = '__dup__archive__index.json';
const INDEX_FILE_SIZE = 2000; // reserver 2K
const EXTRA_FILES_POS_KEY = 'extraPos';
const HEADER_TYPE_NONE = 0;
const HEADER_TYPE_FILE = 1;
const HEADER_TYPE_DIR = 2;
const HEADER_TYPE_GLOB = 3;
/**
* Get header type enum
*
* @param resource $archiveHandle archive resource
*
* @return int
*/
protected static function getNextHeaderType($archiveHandle)
{
$retVal = self::HEADER_TYPE_NONE;
$marker = fgets($archiveHandle, 4);
if (feof($archiveHandle) === false) {
switch ($marker) {
case '<D>':
$retVal = self::HEADER_TYPE_DIR;
break;
case '<F>':
$retVal = self::HEADER_TYPE_FILE;
break;
case '<G>':
$retVal = self::HEADER_TYPE_GLOB;
break;
default:
throw new Exception("Invalid header marker {$marker}. Location:" . ftell($archiveHandle));
}
}
return $retVal;
}
/**
* Get archive index data
*
* @param string $archivePath archive path
*
* @return bool|array return index data, false if don't exists
*/
public static function getIndexData($archivePath)
{
try {
$indexContent = self::getSrcFile($archivePath, self::INDEX_FILE_NAME, 0, 3000, false);
if ($indexContent === false) {
return false;
}
$indexData = json_decode(rtrim($indexContent, "\0"), true);
if (!is_array($indexData)) {
return false;
}
} catch (Exception $e) {
return false;
} catch (Error $e) {
return false;
}
return $indexData;
}
/**
* Get extra files offset if set or 0
*
* @param string $archivePath archive path
*
* @return int
*/
public static function getExtraOffset($archivePath)
{
if (($indexData = self::getIndexData($archivePath)) === false) {
return 0;
}
return (isset($indexData[self::EXTRA_FILES_POS_KEY]) ? $indexData[self::EXTRA_FILES_POS_KEY] : 0);
}
/**
* Add file in archive from src
*
* @param string $archivePath archive path
* @param string $relativePath relative path
* @param int $offset start search location
* @param int $sizeToSearch max size where search
*
* @return bool|int false if file not found of path position
*/
public static function seachPathInArchive($archivePath, $relativePath, $offset = 0, $sizeToSearch = 0)
{
if (($archiveHandle = fopen($archivePath, 'rb')) === false) {
throw new Exception("Cant open archive at $archivePath!");
}
$result = self::searchPath($archivePath, $relativePath, $offset, $sizeToSearch);
@fclose($archiveHandle);
return $result;
}
/**
* Search path, if found set and return position
*
* @param resource $archiveHandle dup archive resource
* @param string $relativePath relative path to extract
* @param int $offset start search location
* @param int $sizeToSearch max size where search
*
* @return bool|int false if file not found of path position
*/
public static function searchPath($archiveHandle, $relativePath, $offset = 0, $sizeToSearch = 0)
{
if (!is_resource($archiveHandle)) {
throw new Exception('Archive handle must be a resource');
}
if (fseek($archiveHandle, $offset, SEEK_SET) < 0) {
return false;
}
if ($offset == 0) {
DupArchiveReaderHeader::readFromArchive($archiveHandle);
}
$result = false;
$position = ftell($archiveHandle);
$continue = true;
do {
switch (($type = self::getNextHeaderType($archiveHandle))) {
case self::HEADER_TYPE_FILE:
$currentFileHeader = DupArchiveReaderFileHeader::readFromArchive($archiveHandle, true, true);
if ($currentFileHeader->relativePath == $relativePath) {
$continue = false;
$result = $position;
}
break;
case self::HEADER_TYPE_DIR:
$directoryHeader = DupArchiveReaderDirectoryHeader::readFromArchive($archiveHandle, true);
if ($directoryHeader->relativePath == $relativePath) {
$continue = false;
$result = $position;
}
break;
case self::HEADER_TYPE_NONE:
$continue = false;
break;
default:
throw new Exception('Invali header type "' . $type . '"');
}
$position = ftell($archiveHandle);
if ($sizeToSearch > 0 && ($position - $offset) >= $sizeToSearch) {
break;
}
} while ($continue);
if ($result !== false) {
if (fseek($archiveHandle, $result, SEEK_SET) < 0) {
return false;
}
}
return $result;
}
/**
* Get file content
*
* @param string $archivePath archvie path
* @param string $relativePath relative path to extract
* @param int $offset start search location
* @param int $sizeToSearch max size where search
* @param bool $isCompressed true if is compressed
*
* @return bool|string false if file not found
*/
public static function getSrcFile($archivePath, $relativePath, $offset = 0, $sizeToSearch = 0, $isCompressed = null)
{
if (($archiveHandle = fopen($archivePath, 'rb')) === false) {
throw new Exception("Cant open archive at $archivePath!");
}
$archiveHeader = DupArchiveReaderHeader::readFromArchive($archiveHandle);
if (is_null($isCompressed)) {
$isCompressed = $archiveHeader->isCompressed;
}
if (self::searchPath($archiveHandle, $relativePath, $offset, $sizeToSearch) === false) {
return false;
}
if (self::getNextHeaderType($archiveHandle) != self::HEADER_TYPE_FILE) {
return false;
}
$header = DupArchiveReaderFileHeader::readFromArchive($archiveHandle, false, true);
$result = self::getSrcFromHeader($archiveHandle, $header, $isCompressed);
@fclose($archiveHandle);
return $result;
}
/**
* Get src file form header
*
* @param resource $archiveHandle archive handle
* @param DupArchiveReaderFileHeader $fileHeader file header
* @param bool $isCompressed true if is compressed
*
* @return string
*/
protected static function getSrcFromHeader($archiveHandle, DupArchiveReaderFileHeader $fileHeader, $isCompressed)
{
if ($fileHeader->fileSize == 0) {
return '';
}
$dataSize = 0;
$result = '';
do {
$globHeader = DupArchiveReaderGlobHeader::readFromArchive($archiveHandle);
$result .= DupArchiveReaderGlobHeader::readContent($archiveHandle, $globHeader, $isCompressed);
$dataSize += $globHeader->originalSize;
} while ($dataSize < $fileHeader->fileSize);
return $result;
}
/**
* Skip file in archive
*
* @param resource $archiveHandle dup archive resource
* @param DupArchiveFileHeader $fileHeader file header
*
* @return void
*/
protected static function skipFileInArchive($archiveHandle, DupArchiveReaderFileHeader $fileHeader)
{
if ($fileHeader->fileSize == 0) {
return;
}
$dataSize = 0;
do {
$globHeader = DupArchiveReaderGlobHeader::readFromArchive($archiveHandle, true);
$dataSize += $globHeader->originalSize;
} while ($dataSize < $fileHeader->fileSize);
}
/**
* Assumes we are on one header and just need to get to the next
*
* @param resource $archiveHandle dup archive resource
*
* @return void
*/
protected static function skipToNextHeader($archiveHandle)
{
$headerType = self::getNextHeaderType($archiveHandle);
switch ($headerType) {
case self::HEADER_TYPE_FILE:
$fileHeader = DupArchiveReaderFileHeader::readFromArchive($archiveHandle, false, true);
self::skipFileInArchive($archiveHandle, $fileHeader);
break;
case self::HEADER_TYPE_DIR:
DupArchiveReaderDirectoryHeader::readFromArchive($archiveHandle, true);
break;
case self::HEADER_TYPE_NONE:
false;
}
}
}

View File

@@ -0,0 +1,827 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\DupArchive;
use Duplicator\Libs\DupArchive\Headers\DupArchiveDirectoryHeader;
use Duplicator\Libs\DupArchive\Headers\DupArchiveFileHeader;
use Duplicator\Libs\DupArchive\Headers\DupArchiveHeader;
use Duplicator\Libs\DupArchive\Headers\DupArchiveReaderFileHeader;
use Duplicator\Libs\DupArchive\Headers\DupArchiveReaderGlobHeader;
use Duplicator\Libs\DupArchive\Info\DupArchiveInfo;
use Duplicator\Libs\DupArchive\Processors\DupArchiveDirectoryProcessor;
use Duplicator\Libs\DupArchive\Processors\DupArchiveFileProcessor;
use Duplicator\Libs\DupArchive\Processors\DupArchiveProcessingFailure;
use Duplicator\Libs\DupArchive\States\DupArchiveCreateState;
use Duplicator\Libs\DupArchive\States\DupArchiveExpandState;
use Duplicator\Libs\DupArchive\States\DupArchiveSimpleCreateState;
use Duplicator\Libs\DupArchive\States\DupArchiveSimpleExpandState;
use Duplicator\Libs\DupArchive\Utils\DupArchiveScanUtil;
use Duplicator\Libs\DupArchive\Utils\DupArchiveUtil;
use Duplicator\Libs\Snap\Snap32BitSizeLimitException;
use Duplicator\Libs\Snap\SnapIO;
use Exception;
use stdClass;
/**
* $re = '/\/\/.* /';
* $subst = '';
*
* $re = '/(.*^\s*)(namespace.*?)(;)(.*)/sm';
* $subst = '$2 { $4}';
*
* $re = '/\/\*.*?\*\//s'; ''
* $re = '/\n\s*\n/s'; "\n"
*/
class DupArchiveEngine extends DupArchive
{
const EXCEPTION_NON_FATAL = 0;
const EXCEPTION_FATAL = 1;
/** @var string|null */
public static $targetRootPath = null;
/**
* Dup archive init
*
* @param DupArchiveLoggerBase $logger logger object
* @param string|null $targetRootPath archive target root path or null not root path
*
* @return void
*/
public static function init(DupArchiveLoggerBase $logger, $targetRootPath = null)
{
DupArchiveUtil::$logger = $logger;
self::$targetRootPath = $targetRootPath;
}
/**
* Get local path
*
* @param string $path item path
* @param DupArchiveCreateState $createState base path
*
* @return string
*/
protected static function getLocalPath($path, DupArchiveCreateState $createState)
{
$result = '';
if (self::$targetRootPath === null) {
$result = substr($path, $createState->basepathLength);
$result = ltrim($result, '/');
if ($createState->newBasePath !== null) {
$result = $createState->newBasePath . $result;
}
} else {
$safePath = SnapIO::safePathUntrailingslashit($path);
$result = ltrim(
$createState->newBasePath . preg_replace('/^' . preg_quote(self::$targetRootPath, '/') . '(.*)/m', '$1', $safePath),
'/'
);
}
return $result;
}
/**
* Get archvie info from path
*
* @param string $filepath archvie path
*
* @return DupArchiveInfo
*/
public static function getArchiveInfo($filepath)
{
$archiveInfo = new DupArchiveInfo();
DupArchiveUtil::log("archive size=" . filesize($filepath));
$archiveHandle = SnapIO::fopen($filepath, 'rb');
$archiveInfo->archiveHeader = DupArchiveHeader::readFromArchive($archiveHandle);
$moreToRead = true;
while ($moreToRead) {
$headerType = self::getNextHeaderType($archiveHandle);
// DupArchiveUtil::log("next header type=$headerType: " . ftell($archiveHandle));
switch ($headerType) {
case self::HEADER_TYPE_FILE:
$fileHeader = DupArchiveFileHeader::readFromArchive($archiveHandle, true, true);
$archiveInfo->fileHeaders[] = $fileHeader;
DupArchiveUtil::log("file" . $fileHeader->relativePath);
break;
case self::HEADER_TYPE_DIR:
$directoryHeader = DupArchiveDirectoryHeader::readFromArchive($archiveHandle, true);
$archiveInfo->directoryHeaders[] = $directoryHeader;
break;
case self::HEADER_TYPE_NONE:
$moreToRead = false;
}
}
return $archiveInfo;
}
/**
* Add folder to archive
*
* can't span requests since create state can't store list of files
*
* @param string $archiveFilepath archive file
* @param string $directory folder to add
* @param string $basepath base path to consider (?)
* @param boolean $includeFiles if true include files
* @param string $newBasepath new base path
* @param int $globSize global size
*
* @return stdClass
*/
public static function addDirectoryToArchiveST(
$archiveFilepath,
$directory,
$basepath,
$includeFiles = false,
$newBasepath = null,
$globSize = DupArchiveCreateState::DEFAULT_GLOB_SIZE
) {
if ($includeFiles) {
$scan = DupArchiveScanUtil::createScanObject($directory);
} else {
$scan = new stdClass();
$scan->Files = array();
$scan->Dirs = array();
}
$createState = new DupArchiveSimpleCreateState();
$createState->archiveOffset = filesize($archiveFilepath);
$createState->archivePath = $archiveFilepath;
$createState->basePath = $basepath;
$createState->basepathLength = strlen($basepath);
$createState->timerEnabled = false;
$createState->globSize = $globSize;
$createState->newBasePath = $newBasepath;
self::addItemsToArchive($createState, $scan);
$retVal = new stdClass();
$retVal->numDirsAdded = $createState->currentDirectoryIndex;
$retVal->numFilesAdded = $createState->currentFileIndex;
if ($createState->skippedFileCount > 0) {
throw new Exception("One or more files were were not able to be added when adding {$directory} to {$archiveFilepath}");
} elseif ($createState->skippedDirectoryCount > 0) {
throw new Exception("One or more directories were not able to be added when adding {$directory} to {$archiveFilepath}");
}
return $retVal;
}
/**
* Add relative file to archive
*
* @param string $archiveFilepath archive file
* @param string $filepath file to add
* @param string $relativePath relative path in archive
* @param int $globSize global size
*
* @return void
*/
public static function addRelativeFileToArchiveST(
$archiveFilepath,
$filepath,
$relativePath,
$globSize = DupArchiveCreateState::DEFAULT_GLOB_SIZE
) {
$createState = new DupArchiveSimpleCreateState();
$createState->archiveOffset = filesize($archiveFilepath);
$createState->archivePath = $archiveFilepath;
$createState->basePath = null;
$createState->basepathLength = 0;
$createState->timerEnabled = false;
$createState->globSize = $globSize;
$scan = new stdClass();
$scan->Files = array();
$scan->Dirs = array();
$scan->Files[] = $filepath;
if ($relativePath != null) {
$scan->FileAliases = array();
$scan->FileAliases[$filepath] = $relativePath;
}
self::addItemsToArchive($createState, $scan);
}
/**
* Add file in archive from src
*
* @param string|resource $archive Archive path or archive handle
* @param string $src source string
* @param string $relativeFilePath relative path
* @param int $forceSize if 0 size is auto of content is filled of \0 char to size
*
* @return bool
*/
public static function addFileFromSrc(
$archive,
$src,
$relativeFilePath,
$forceSize = 0
) {
if (is_resource($archive)) {
$archiveHandle = $archive;
SnapIO::fseek($archiveHandle, 0, SEEK_SET);
} else {
if (($archiveHandle = SnapIO::fopen($archive, 'r+b')) == false) {
throw new Exception('Can\'t open archive');
}
}
$createState = new DupArchiveSimpleCreateState();
$createState->archiveOffset = SnapIO::ftell($archiveHandle);
$createState->basePath = dirname($relativeFilePath);
$createState->basepathLength = strlen($createState->basePath);
$createState->timerEnabled = false;
if ($forceSize == 0) {
$archiveHeader = DupArchiveHeader::readFromArchive($archiveHandle);
$createState->isCompressed = $archiveHeader->isCompressed;
} else {
// ff force size is enables the src isn't compress
$createState->isCompressed = false;
}
SnapIO::fseek($archiveHandle, 0, SEEK_END);
$result = DupArchiveFileProcessor::writeFileSrcToArchive($createState, $archiveHandle, $src, $relativeFilePath, $forceSize);
if (!is_resource($archive)) {
SnapIO::fclose($archiveHandle);
}
return $result;
}
/**
* Add file in archive from src
*
* @param string $archiveFilepath archive path
* @param string $src source string
* @param string $relativeFilePath relative path
* @param int $offset start search location
* @param int $sizeToSearch max size where search
*
* @return bool
*/
public static function replaceFileContent(
$archiveFilepath,
$src,
$relativeFilePath,
$offset = 0,
$sizeToSearch = 0
) {
if (($archiveHandle = SnapIO::fopen($archiveFilepath, 'r+b')) == false) {
throw new Exception('Can\'t open archive');
}
if (($filePos = self::searchPath($archiveHandle, $relativeFilePath, $offset, $sizeToSearch)) == false) {
return false;
}
$fileHeader = DupArchiveReaderFileHeader::readFromArchive($archiveHandle);
$globHeader = DupArchiveReaderGlobHeader::readFromArchive($archiveHandle);
SnapIO::fseek($archiveHandle, $filePos);
$createState = new DupArchiveSimpleCreateState();
$createState->archivePath = $archiveFilepath;
$createState->archiveOffset = $filePos;
$createState->basePath = dirname($relativeFilePath);
$createState->basepathLength = strlen($createState->basePath);
$createState->timerEnabled = false;
$createState->isCompressed = false; // replaced content can't be compressed
$forceSize = $globHeader->storedSize;
$result = DupArchiveFileProcessor::writeFileSrcToArchive($createState, $archiveHandle, $src, $relativeFilePath, $forceSize);
SnapIO::fclose($archiveHandle);
return $result;
}
/**
* Add file in archive using base dir
*
* @param string $archiveFilepath archive file
* @param string $basePath base path
* @param string $filepath file to add
* @param int $globSize global size
*
* @return void
*/
public static function addFileToArchiveUsingBaseDirST(
$archiveFilepath,
$basePath,
$filepath,
$globSize = DupArchiveCreateState::DEFAULT_GLOB_SIZE
) {
$createState = new DupArchiveSimpleCreateState();
$createState->archiveOffset = filesize($archiveFilepath);
$createState->archivePath = $archiveFilepath;
$createState->basePath = $basePath;
$createState->basepathLength = strlen($basePath);
$createState->timerEnabled = false;
$createState->globSize = $globSize;
$scan = new stdClass();
$scan->Files = array();
$scan->Dirs = array();
$scan->Files[] = $filepath;
self::addItemsToArchive($createState, $scan);
}
/**
* Create archive
*
* @param string $archivePath archive file path
* @param bool $isCompressed is compressed
*
* @return void
*/
public static function createArchive($archivePath, $isCompressed)
{
if (($archiveHandle = SnapIO::fopen($archivePath, 'w+b')) === false) {
throw new Exception('Can\t create dup archvie file ' . $archivePath);
}
$archiveHeader = DupArchiveHeader::create($isCompressed);
$archiveHeader->writeToArchive($archiveHandle);
//reserver space for index
$src = json_encode(array('test'));
$src .= str_repeat("\0", self::INDEX_FILE_SIZE - strlen($src));
self::addFileFromSrc($archiveHandle, $src, self::INDEX_FILE_NAME, self::INDEX_FILE_SIZE);
// Intentionally do not write build state since if something goes wrong we went it to start over on the archive
SnapIO::fclose($archiveHandle);
}
/**
* Add items to archive
*
* @param DupArchiveCreateState $createState create state info
* @param stdClass $scanFSInfo scan if
*
* @return void
*/
public static function addItemsToArchive(DupArchiveCreateState $createState, stdClass $scanFSInfo)
{
if ($createState->globSize == -1) {
$createState->globSize = DupArchiveCreateState::DEFAULT_GLOB_SIZE;
}
DupArchiveUtil::tlogObject("addItemsToArchive start", $createState);
$directoryCount = count($scanFSInfo->Dirs);
$fileCount = count($scanFSInfo->Files);
$createState->startTimer();
$archiveHandle = SnapIO::fopen($createState->archivePath, 'r+b');
DupArchiveUtil::tlog("Archive size=", filesize($createState->archivePath));
DupArchiveUtil::tlog("Archive location is now " . SnapIO::ftell($archiveHandle));
$archiveHeader = DupArchiveHeader::readFromArchive($archiveHandle);
$createState->isCompressed = $archiveHeader->isCompressed;
if ($createState->archiveOffset == filesize($createState->archivePath)) {
DupArchiveUtil::tlog(
"Seeking to end of archive location because of offset {$createState->archiveOffset} " .
"for file size " . filesize($createState->archivePath)
);
SnapIO::fseek($archiveHandle, 0, SEEK_END);
} else {
DupArchiveUtil::tlog("Seeking archive offset {$createState->archiveOffset} for file size " . filesize($createState->archivePath));
SnapIO::fseek($archiveHandle, $createState->archiveOffset);
}
while (($createState->currentDirectoryIndex < $directoryCount) && (!$createState->timedOut())) {
if ($createState->throttleDelayInUs !== 0) {
usleep($createState->throttleDelayInUs);
}
$directory = $scanFSInfo->Dirs[$createState->currentDirectoryIndex];
try {
$relativeDirectoryPath = '';
if (isset($scanFSInfo->DirectoryAliases) && array_key_exists($directory, $scanFSInfo->DirectoryAliases)) {
$relativeDirectoryPath = $scanFSInfo->DirectoryAliases[$directory];
} else {
$relativeDirectoryPath = self::getLocalPath($directory, $createState);
}
if ($relativeDirectoryPath !== '') {
DupArchiveDirectoryProcessor::writeDirectoryToArchive($createState, $archiveHandle, $directory, $relativeDirectoryPath);
} else {
$createState->skippedDirectoryCount++;
$createState->currentDirectoryIndex++;
}
} catch (Exception $ex) {
DupArchiveUtil::log("Failed to add {$directory} to archive. Error: " . $ex->getMessage(), true);
$createState->addFailure(DupArchiveProcessingFailure::TYPE_DIRECTORY, $directory, $ex->getMessage(), false);
$createState->currentDirectoryIndex++;
$createState->skippedDirectoryCount++;
$createState->save();
}
}
$createState->archiveOffset = SnapIO::ftell($archiveHandle);
$workTimestamp = time();
while (($createState->currentFileIndex < $fileCount) && (!$createState->timedOut())) {
$filepath = $scanFSInfo->Files[$createState->currentFileIndex];
try {
$relativeFilePath = '';
if (isset($scanFSInfo->FileAliases) && array_key_exists($filepath, $scanFSInfo->FileAliases)) {
$relativeFilePath = $scanFSInfo->FileAliases[$filepath];
} else {
$relativeFilePath = self::getLocalPath($filepath, $createState);
}
// Uncomment when testing error handling
// if((strpos($relativeFilePath, 'dup-installer') !== false) || (strpos($relativeFilePath, 'lib') !== false)) {
// Dup_Log::Trace("Was going to do intentional error to {$relativeFilePath} but skipping");
// } else {
// throw new Exception("#### intentional file error when writing " . $relativeFilePath);
// }
// }
DupArchiveFileProcessor::writeFilePortionToArchive($createState, $archiveHandle, $filepath, $relativeFilePath);
if (($createState->isRobust) && (time() - $workTimestamp >= 1)) {
DupArchiveUtil::log("Robust mode create state save");
// When in robustness mode save the state every second
$workTimestamp = time();
$createState->working = ($createState->currentDirectoryIndex < $directoryCount) || ($createState->currentFileIndex < $fileCount);
$createState->save();
}
} catch (Snap32BitSizeLimitException $ex) {
throw $ex;
} catch (Exception $ex) {
DupArchiveUtil::log("Failed to add {$filepath} to archive. Error: " . $ex->getMessage() . $ex->getTraceAsString(), true);
$createState->currentFileIndex++;
$createState->skippedFileCount++;
$createState->addFailure(DupArchiveProcessingFailure::TYPE_FILE, $filepath, $ex->getMessage(), ($ex->getCode() === self::EXCEPTION_FATAL));
$createState->save();
}
}
$createState->working = ($createState->currentDirectoryIndex < $directoryCount) || ($createState->currentFileIndex < $fileCount);
$createState->save();
SnapIO::fclose($archiveHandle);
if (!$createState->working) {
DupArchiveUtil::log("compress done");
} else {
DupArchiveUtil::tlog("compress not done so continuing later");
}
}
/**
* Expand archive
*
* @param DupArchiveExpandState $expandState expand state
*
* @return void
*/
public static function expandArchive(DupArchiveExpandState $expandState)
{
$expandState->startTimer();
$archiveHandle = SnapIO::fopen($expandState->archivePath, 'rb');
SnapIO::fseek($archiveHandle, $expandState->archiveOffset);
if ($expandState->archiveOffset == 0) {
$expandState->archiveHeader = DupArchiveHeader::readFromArchive($archiveHandle);
$expandState->isCompressed = $expandState->archiveHeader->isCompressed;
$expandState->archiveOffset = SnapIO::ftell($archiveHandle);
$expandState->save();
} else {
DupArchiveUtil::log("#### seeking archive offset {$expandState->archiveOffset}");
}
DupArchiveUtil::log('DUP EXPAND OFFSET ' . $expandState->archiveOffset);
if ((!$expandState->validateOnly) || ($expandState->validationType == DupArchiveExpandState::VALIDATION_FULL)) {
$moreItems = self::expandItems($expandState, $archiveHandle);
} else {
$moreItems = self::standardValidateItems($expandState, $archiveHandle);
}
$expandState->working = $moreItems;
$expandState->save();
SnapIO::fclose($archiveHandle, false);
if (!$expandState->working) {
DupArchiveUtil::log("DUP EXPAND DONE");
if (($expandState->expectedFileCount != -1) && ($expandState->expectedFileCount != $expandState->fileWriteCount)) {
$expandState->addFailure(
DupArchiveProcessingFailure::TYPE_FILE,
'Archive',
"Number of files expected ({$expandState->expectedFileCount}) doesn't equal number written ({$expandState->fileWriteCount})."
);
}
if (($expandState->expectedDirectoryCount != -1) && ($expandState->expectedDirectoryCount != $expandState->directoryWriteCount)) {
$expandState->addFailure(
DupArchiveProcessingFailure::TYPE_DIRECTORY,
'Archive',
"Number of directories expected ({$expandState->expectedDirectoryCount}) " .
"doesn't equal number written ({$expandState->directoryWriteCount})."
);
}
} else {
DupArchiveUtil::tlogObject("expand not done so continuing later", $expandState);
}
}
/**
* Single-threaded file expansion
*
* @param string $archiveFilePath archive path
* @param string $relativeFilePaths relative file path in archive
* @param string $destPath destination path
*
* @return void
*/
public static function expandFiles($archiveFilePath, $relativeFilePaths, $destPath)
{
// Not setting timeout timestamp so it will never timeout
DupArchiveUtil::tlog("opening archive {$archiveFilePath}");
$archiveHandle = SnapIO::fopen($archiveFilePath, 'r');
/* @var $expandState DupArchiveSimpleExpandState */
$expandState = new DupArchiveSimpleExpandState();
$expandState->archiveHeader = DupArchiveHeader::readFromArchive($archiveHandle);
$expandState->isCompressed = $expandState->archiveHeader->isCompressed;
$expandState->archiveOffset = SnapIO::ftell($archiveHandle);
$expandState->includedFiles = $relativeFilePaths;
$expandState->filteredDirectories = array('*');
$expandState->filteredFiles = array('*');
// $expandState->basePath = $destPath . '/tempExtract'; // RSR remove once extract works
$expandState->basePath = $destPath; // RSR remove once extract works
// TODO: Filter out all directories/files except those in the list
self::expandItems($expandState, $archiveHandle);
}
/**
* Expand dup archive items
*
* @param DupArchiveExpandState $expandState dup archive expand state
* @param resource $archiveHandle dup archvie resource
*
* @return bool true if more to read
*/
private static function expandItems(DupArchiveExpandState $expandState, $archiveHandle)
{
$moreToRead = true;
$workTimestamp = time();
while ($moreToRead && (!$expandState->timedOut())) {
if ($expandState->throttleDelayInUs !== 0) {
usleep($expandState->throttleDelayInUs);
}
if ($expandState->currentFileHeader != null) {
DupArchiveUtil::tlog("Writing file {$expandState->currentFileHeader->relativePath}");
if (self::filePassesFilters($expandState)) {
try {
$fileCompleted = DupArchiveFileProcessor::writeToFile($expandState, $archiveHandle);
} catch (Exception $ex) {
DupArchiveUtil::log("Failed to write to {$expandState->currentFileHeader->relativePath}. Error: " . $ex->getMessage(), true);
// Reset things - skip over this file within the archive.
SnapIO::fseek($archiveHandle, $expandState->lastHeaderOffset);
self::skipToNextHeader($archiveHandle, $expandState->currentFileHeader);
$expandState->archiveOffset = ftell($archiveHandle);
$expandState->addFailure(
DupArchiveProcessingFailure::TYPE_FILE,
$expandState->currentFileHeader->relativePath,
$ex->getMessage(),
false
);
$expandState->resetForFile();
$expandState->lastHeaderOffset = -1;
$expandState->save();
}
} else {
self::skipFileInArchive($archiveHandle, $expandState->currentFileHeader);
$expandState->resetForFile();
}
} else {
// Header is null so read in the next one
$expandState->lastHeaderOffset = @ftell($archiveHandle);
$headerType = self::getNextHeaderType($archiveHandle);
DupArchiveUtil::tlog('header type ' . $headerType);
switch ($headerType) {
case self::HEADER_TYPE_FILE:
DupArchiveUtil::tlog('File header');
$expandState->currentFileHeader = DupArchiveFileHeader::readFromArchive($archiveHandle, false, true);
$expandState->archiveOffset = @ftell($archiveHandle);
DupArchiveUtil::tlog('Just read file header from archive');
break;
case self::HEADER_TYPE_DIR:
DupArchiveUtil::tlog('Directory Header');
$directoryHeader = DupArchiveDirectoryHeader::readFromArchive($archiveHandle, true);
if (self::passesDirectoryExclusion($expandState, $directoryHeader->relativePath)) {
$createdDirectory = true;
if (!$expandState->validateOnly) {
$createdDirectory = DupArchiveFileProcessor::createDirectory($expandState, $directoryHeader);
}
if ($createdDirectory) {
$expandState->directoryWriteCount++;
}
}
$expandState->archiveOffset = ftell($archiveHandle);
DupArchiveUtil::tlog('Just read directory header ' . $directoryHeader->relativePath . ' from archive');
break;
case self::HEADER_TYPE_NONE:
$moreToRead = false;
}
}
if (($expandState->isRobust) && (time() - $workTimestamp >= 1)) {
DupArchiveUtil::log("Robust mode extract state save for standard validate");
// When in robustness mode save the state every second
$workTimestamp = time();
$expandState->save();
}
}
$expandState->save();
return $moreToRead;
}
/**
* check exclude dir
*
* @param DupArchiveExpandState $expandState dup archive expand state
* @param string $candidate check exclude dir
*
* @return bool
*/
private static function passesDirectoryExclusion(DupArchiveExpandState $expandState, $candidate)
{
foreach ($expandState->filteredDirectories as $directoryFilter) {
if ($directoryFilter === '*') {
return false;
}
if (SnapIO::getRelativePath($candidate, $directoryFilter) !== false) {
return false;
}
}
if (in_array($candidate, $expandState->excludedDirWithoutChilds)) {
return false;
}
return true;
}
/**
* Check flils filters
*
* @param DupArchiveExpandState $expandState dup archive expand state
*
* @return boolean
*/
private static function filePassesFilters(DupArchiveExpandState $expandState)
{
$candidate = $expandState->currentFileHeader->relativePath;
// Included files trumps all exclusion filters
foreach ($expandState->includedFiles as $includedFile) {
if ($includedFile === $candidate) {
return true;
}
}
if (self::passesDirectoryExclusion($expandState, $candidate)) {
foreach ($expandState->filteredFiles as $fileFilter) {
if ($fileFilter === '*' || $fileFilter === $candidate) {
return false;
}
}
} else {
return false;
}
return true;
}
/**
* Validate items
*
* @param DupArchiveExpandState $expandState dup archive expan state
* @param resource $archiveHandle dup archive resource
*
* @return bool true if more to read
*/
private static function standardValidateItems(DupArchiveExpandState $expandState, $archiveHandle)
{
$moreToRead = true;
$to = $expandState->timedOut();
$workTimestamp = time();
while ($moreToRead && (!$to)) {
if ($expandState->throttleDelayInUs !== 0) {
usleep($expandState->throttleDelayInUs);
}
if ($expandState->currentFileHeader != null) {
try {
$fileCompleted = DupArchiveFileProcessor::standardValidateFileEntry($expandState, $archiveHandle);
if ($fileCompleted) {
$expandState->resetForFile();
}
// Expand state taken care of within the write to file to ensure consistency
} catch (Exception $ex) {
DupArchiveUtil::log("Failed validate file in archive. Error: " . $ex->getMessage(), true);
DupArchiveUtil::logObject("expand state", $expandState, true);
// $expandState->currentFileIndex++;
// RSR TODO: Need way to skip past that file
$expandState->addFailure(DupArchiveProcessingFailure::TYPE_FILE, $expandState->currentFileHeader->relativePath, $ex->getMessage());
$expandState->save();
$moreToRead = false;
}
} else {
$headerType = self::getNextHeaderType($archiveHandle);
switch ($headerType) {
case self::HEADER_TYPE_FILE:
$expandState->currentFileHeader = DupArchiveFileHeader::readFromArchive($archiveHandle, false, true);
$expandState->archiveOffset = ftell($archiveHandle);
break;
case self::HEADER_TYPE_DIR:
$directoryHeader = DupArchiveDirectoryHeader::readFromArchive($archiveHandle, true);
$expandState->directoryWriteCount++;
$expandState->archiveOffset = ftell($archiveHandle);
break;
case self::HEADER_TYPE_NONE:
$moreToRead = false;
}
}
if (($expandState->isRobust) && (time() - $workTimestamp >= 1)) {
DupArchiveUtil::log("Robust mdoe extract state save for standard validate");
// When in robustness mode save the state every second
$workTimestamp = time();
$expandState->save();
}
$to = $expandState->timedOut();
}
$expandState->save();
return $moreToRead;
}
}

View File

@@ -0,0 +1,272 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\DupArchive;
use Duplicator\Libs\DupArchive\Headers\DupArchiveReaderDirectoryHeader;
use Duplicator\Libs\DupArchive\Headers\DupArchiveReaderFileHeader;
use Duplicator\Libs\DupArchive\Headers\DupArchiveReaderGlobHeader;
use Duplicator\Libs\DupArchive\Headers\DupArchiveReaderHeader;
use Duplicator\Libs\DupArchive\Info\DupArchiveExpanderInfo;
use Exception;
class DupArchiveExpandBasicEngine extends DupArchive
{
protected static $logCallback = null;
protected static $chmodCallback = null;
protected static $mkdirCallback = null;
/**
* Set callabcks function
*
* @param null|callable $log function callback
* @param null|callable $chmod function callback
* @param null|callable $mkdir function callback
*
* @return void
*/
public static function setCallbacks($log, $chmod, $mkdir)
{
self::$logCallback = (is_callable($log) ? $log : null);
self::$chmodCallback = (is_callable($chmod) ? $chmod : null);
self::$mkdirCallback = (is_callable($mkdir) ? $mkdir : null);
}
/**
* Write log
*
* @param string $s string
* @param bool $flush if true flush file
*
* @return void
*/
public static function log($s, $flush = false)
{
if (self::$logCallback == null) {
return;
}
call_user_func(self::$logCallback, "MINI EXPAND:$s", $flush);
}
/**
* Expand folder
*
* @param string $archivePath archive path
* @param string $relativePath relative path
* @param string $destPath dest path
* @param bool $ignoreErrors if true ignore errors
* @param int $offset start scan location
*
* @return void
*/
public static function expandDirectory($archivePath, $relativePath, $destPath, $ignoreErrors = false, $offset = 0)
{
self::expandItems($archivePath, $relativePath, $destPath, $ignoreErrors, $offset);
}
/**
* Expand items
*
* @param string $archivePath archive path
* @param string[] $inclusionFilter filters
* @param string $destDirectory dest path
* @param bool $ignoreErrors if true ignore errors
* @param int $offset start scan location
*
* @return void
*/
private static function expandItems($archivePath, $inclusionFilter, $destDirectory, $ignoreErrors = false, $offset = 0)
{
$archiveHandle = fopen($archivePath, 'rb');
if ($archiveHandle === false) {
throw new Exception("Cant open archive at $archivePath!");
}
$archiveHeader = DupArchiveReaderHeader::readFromArchive($archiveHandle);
$writeInfo = new DupArchiveExpanderInfo();
$writeInfo->destDirectory = $destDirectory;
$writeInfo->isCompressed = $archiveHeader->isCompressed;
if ($offset > 0) {
fseek($archiveHandle, $offset);
}
$moreToRead = true;
while ($moreToRead) {
if ($writeInfo->currentFileHeader != null) {
try {
if (self::passesInclusionFilter($inclusionFilter, $writeInfo->currentFileHeader->relativePath)) {
self::writeToFile($archiveHandle, $writeInfo);
$writeInfo->fileWriteCount++;
} elseif ($writeInfo->currentFileHeader->fileSize > 0) {
self::skipFileInArchive($archiveHandle, $writeInfo->currentFileHeader);
}
$writeInfo->currentFileHeader = null;
// Expand state taken care of within the write to file to ensure consistency
} catch (Exception $ex) {
if (!$ignoreErrors) {
throw $ex;
}
}
} else {
$headerType = self::getNextHeaderType($archiveHandle);
switch ($headerType) {
case self::HEADER_TYPE_FILE:
$writeInfo->currentFileHeader = DupArchiveReaderFileHeader::readFromArchive($archiveHandle, false, true);
break;
case self::HEADER_TYPE_DIR:
$directoryHeader = DupArchiveReaderDirectoryHeader::readFromArchive($archiveHandle, true);
// self::log("considering $inclusionFilter and {$directoryHeader->relativePath}");
if (self::passesInclusionFilter($inclusionFilter, $directoryHeader->relativePath)) {
// self::log("passed");
$directory = "{$writeInfo->destDirectory}/{$directoryHeader->relativePath}";
// $mode = $directoryHeader->permissions;
// rodo handle this more elegantly @mkdir($directory, $directoryHeader->permissions, true);
if (is_callable(self::$mkdirCallback)) {
call_user_func(self::$mkdirCallback, $directory, 'u+rwx', true);
} else {
mkdir($directory, 0755, true);
}
$writeInfo->directoryWriteCount++;
} else {
// self::log("didnt pass");
}
break;
case self::HEADER_TYPE_NONE:
$moreToRead = false;
}
}
}
fclose($archiveHandle);
}
/**
* Write to file
*
* @param resource $archiveHandle archive file handle
* @param DupArchiveExpanderInfo $writeInfo write info
*
* @return void
*/
private static function writeToFile($archiveHandle, DupArchiveExpanderInfo $writeInfo)
{
$destFilePath = $writeInfo->getCurrentDestFilePath();
if ($writeInfo->currentFileHeader->fileSize > 0) {
$parentDir = dirname($destFilePath);
if (!file_exists($parentDir)) {
if (is_callable(self::$mkdirCallback)) {
$res = call_user_func(self::$mkdirCallback, $parentDir, 'u+rwx', true);
} else {
$res = mkdir($parentDir, 0755, true);
}
if (!$res) {
throw new Exception("Couldn't create {$parentDir}");
}
}
$destFileHandle = fopen($destFilePath, 'wb+');
if ($destFileHandle === false) {
throw new Exception("Couldn't open {$destFilePath} for writing.");
}
do {
self::appendGlobToFile($archiveHandle, $destFileHandle, $writeInfo);
$currentFileOffset = ftell($destFileHandle);
$moreGlobstoProcess = $currentFileOffset < $writeInfo->currentFileHeader->fileSize;
} while ($moreGlobstoProcess);
fclose($destFileHandle);
if (is_callable(self::$chmodCallback)) {
call_user_func(self::$chmodCallback, $destFilePath, 'u+rw');
} else {
chmod($destFilePath, 0644);
}
self::validateExpandedFile($writeInfo);
} else {
if (touch($destFilePath) === false) {
throw new Exception("Couldn't create $destFilePath");
}
if (is_callable(self::$chmodCallback)) {
call_user_func(self::$chmodCallback, $destFilePath, 'u+rw');
} else {
chmod($destFilePath, 0644);
}
}
}
/**
* Validate file
*
* @param DupArchiveExpanderInfo $writeInfo write info
*
* @return void
*/
private static function validateExpandedFile(DupArchiveExpanderInfo $writeInfo)
{
if ($writeInfo->currentFileHeader->hash !== '00000000000000000000000000000000') {
$hash = hash_file('crc32b', $writeInfo->getCurrentDestFilePath());
if ($hash !== $writeInfo->currentFileHeader->hash) {
throw new Exception("MD5 validation fails for {$writeInfo->getCurrentDestFilePath()}");
}
}
}
/**
* Undocumented function
* Assumption is that archive handle points to a glob header on this call
*
* @param resource $archiveHandle archive handle
* @param resource $destFileHandle dest file handle
* @param DupArchiveExpanderInfo $writeInfo write info
*
* @return void
*/
private static function appendGlobToFile($archiveHandle, $destFileHandle, DupArchiveExpanderInfo $writeInfo)
{
$globHeader = DupArchiveReaderGlobHeader::readFromArchive($archiveHandle, false);
$globContents = fread($archiveHandle, $globHeader->storedSize);
if ($globContents === false) {
throw new Exception("Error reading glob from " . $writeInfo->getCurrentDestFilePath());
}
if ($writeInfo->isCompressed) {
$globContents = gzinflate($globContents);
}
if (fwrite($destFileHandle, $globContents) !== strlen($globContents)) {
throw new Exception("Unable to write all bytes of data glob to storage.");
}
}
/**
* Check filter
*
* @param string $filter filter
* @param string $candidate candidate
*
* @return bool
*/
private static function passesInclusionFilter($filter, $candidate)
{
return (substr($candidate, 0, strlen($filter)) == $filter);
}
}

View File

@@ -0,0 +1,23 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\DupArchive;
abstract class DupArchiveLoggerBase
{
/**
* Log function
*
* @param string $s string to log
* @param boolean $flush if true flish log
* @param callback|null $callingFunctionOverride call back function
*
* @return void
*/
abstract public function log($s, $flush = false, $callingFunctionOverride = null);
}

View File

@@ -0,0 +1,50 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\DupArchive\Headers;
use Exception;
// Format
class DupArchiveDirectoryHeader extends DupArchiveReaderDirectoryHeader
{
public $mtime = 0;
public $permissions = '';
public $relativePathLength = 1;
public $relativePath = '';
/**
* Write header in archive
*
* @param resource $archiveHandle archive resource
*
* @return int bytes written
*/
public function writeToArchive($archiveHandle)
{
if ($this->relativePathLength == 0) {
// Don't allow a base path to be written to the archive
return;
}
$headerString = '<D><MT>' .
$this->mtime . '</MT><P>' .
$this->permissions . '</P><RPL>' .
$this->relativePathLength . '</RPL><RP>' .
$this->relativePath . '</RP></D>';
//SnapIO::fwrite($archiveHandle, $headerString);
$bytes_written = @fwrite($archiveHandle, $headerString);
if ($bytes_written === false) {
throw new Exception('Error writing to file.');
} else {
return $bytes_written;
}
}
}

View File

@@ -0,0 +1,119 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\DupArchive\Headers;
use Duplicator\Libs\Snap\SnapIO;
use Exception;
/**
* File header
*/
class DupArchiveFileHeader extends DupArchiveReaderFileHeader
{
const MAX_SIZE_FOR_HASHING = 1000000000;
public $fileSize = 0;
public $mtime = 0;
public $permissions = '';
public $hash = '';
public $relativePathLength = 0;
public $relativePath = '';
/**
* create header from file
*
* @param string $filepath file path
* @param string $relativeFilePath relative file path in archive
*
* @return static
*/
public static function createFromFile($filepath, $relativeFilePath)
{
$instance = new static();
$instance->fileSize = SnapIO::filesize($filepath);
$instance->permissions = substr(sprintf('%o', fileperms($filepath)), -4);
$instance->mtime = SnapIO::filemtime($filepath);
if ($instance->fileSize > self::MAX_SIZE_FOR_HASHING) {
$instance->hash = "00000000000000000000000000000000";
} else {
$instance->hash = hash_file('crc32b', $filepath);
}
$instance->relativePath = $relativeFilePath;
$instance->relativePathLength = strlen($instance->relativePath);
return $instance;
}
/**
* create header from src
*
* @param string $src source string
* @param string $relativeFilePath relative path in archvie
* @param int $forceSize if 0 size is auto of content is filled of \0 char to size
*
* @return static
*/
public static function createFromSrc($src, $relativeFilePath, $forceSize = 0)
{
$instance = new static();
$instance->fileSize = strlen($src);
$instance->permissions = '0644';
$instance->mtime = time();
$srcLen = strlen($src);
if ($forceSize > 0 && $srcLen < $forceSize) {
$charsToAdd = $forceSize - $srcLen;
$src .= str_repeat("\0", $charsToAdd);
}
if ($instance->fileSize > self::MAX_SIZE_FOR_HASHING) {
$instance->hash = "00000000000000000000000000000000";
} else {
$instance->hash = hash('crc32b', $src);
}
$instance->relativePath = $relativeFilePath;
$instance->relativePathLength = strlen($instance->relativePath);
return $instance;
}
/**
* Write header in archive
*
* @param resource $archiveHandle archive resource
*
* @return int bytes written
*/
public function writeToArchive($archiveHandle)
{
$headerString = '<F><FS>' .
$this->fileSize . '</FS><MT>' .
$this->mtime . '</MT><P>' .
$this->permissions . '</P><HA>' .
$this->hash . '</HA><RPL>' .
$this->relativePathLength . '</RPL><RP>' .
$this->relativePath . '</RP></F>';
//SnapIO::fwrite($archiveHandle, $headerString);
$bytes_written = @fwrite($archiveHandle, $headerString);
if ($bytes_written === false) {
throw new Exception('Error writing to file.');
} else {
return $bytes_written;
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\DupArchive\Headers;
use Exception;
/**
* Dup archive glob header
*
* Format
* #C#{$originalSize}#{$storedSize}!
*/
class DupArchiveGlobHeader extends DupArchiveReaderGlobHeader
{
// public $marker;
public $originalSize;
public $storedSize;
public $hash;
/**
* Write header in archive
*
* @param resource $archiveHandle archive file resource
*
* @return int
*/
public function writeToArchive($archiveHandle)
{
// <G><OS>x</OS>x<SS>x</SS><HA>x</HA></G>
$headerString = '<G><OS>' . $this->originalSize . '</OS><SS>' . $this->storedSize . '</SS><HA>' . $this->hash . '</HA></G>';
//SnapIO::fwrite($archiveHandle, $headerString);
$bytes_written = @fwrite($archiveHandle, $headerString);
if ($bytes_written === false) {
throw new Exception('Error writing to file.');
} else {
return $bytes_written;
}
}
}

View File

@@ -0,0 +1,53 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\DupArchive\Headers;
use Duplicator\Libs\DupArchive\DupArchiveEngine;
use Duplicator\Libs\Snap\SnapIO;
use Exception;
/**
* Dup archive header
*
* Format: #A#{version:5}#{isCompressed}!
*/
class DupArchiveHeader extends DupArchiveReaderHeader
{
/** @var string */
protected $version;
/** @var bool */
public $isCompressed;
/**
* Create new header
*
* @param bool $isCompressed true if is compressed
*
* @return self
*/
public static function create($isCompressed)
{
$instance = new self();
$instance->version = DupArchiveEngine::DUPARCHIVE_VERSION;
$instance->isCompressed = $isCompressed;
return $instance;
}
/**
* Write header to archive
*
* @param resource $archiveHandle archive resource
*
* @return void
*/
public function writeToArchive($archiveHandle)
{
SnapIO::fwrite($archiveHandle, '<A><V>' . $this->version . '</V><C>' . ($this->isCompressed ? 'true' : 'false') . '</C></A>');
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\DupArchive\Headers;
use Exception;
class DupArchiveHeaderU
{
const MAX_FILED_LEN = 128;
/**
* Undocumented function
*
* @param resource $archiveHandle archvie resource
* @param int $ename header enum
*
* @return string
*/
public static function readStandardHeaderField($archiveHandle, $ename)
{
$expectedStart = '<' . $ename . '>';
$expectedEnd = '</' . $ename . '>';
$startingElement = fread($archiveHandle, strlen($expectedStart));
if ($startingElement !== $expectedStart) {
throw new Exception("Invalid starting element. Was expecting {$expectedStart} but got {$startingElement}");
}
$headerString = stream_get_line($archiveHandle, self::MAX_FILED_LEN, $expectedEnd);
if ($headerString === false) {
throw new Exception('Error reading line.');
}
return $headerString;
}
}

View File

@@ -0,0 +1,79 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\DupArchive\Headers;
use Exception;
/**
* Class dir header reader
*/
class DupArchiveReaderDirectoryHeader
{
public $mtime = 0;
public $permissions = '';
public $relativePathLength = 1;
public $relativePath = '';
/**
* Class constructor
*/
public function __construct()
{
}
/**
* Read folder from archive
*
* @param resource $archiveHandle archive resource
* @param boolean $skipStartElement if true sckip start element
*
* @return static
*/
public static function readFromArchive($archiveHandle, $skipStartElement = false)
{
$instance = new static();
if (!$skipStartElement) {
// <A>
$startElement = fread($archiveHandle, 3);
if ($startElement === false) {
if (feof($archiveHandle)) {
return false;
} else {
throw new Exception('Error reading directory header');
}
}
if ($startElement != '<D>') {
throw new Exception("Invalid directory header marker found [{$startElement}] : location " . ftell($archiveHandle));
}
}
$instance->mtime = DupArchiveHeaderU::readStandardHeaderField($archiveHandle, 'MT');
$instance->permissions = DupArchiveHeaderU::readStandardHeaderField($archiveHandle, 'P');
$instance->relativePathLength = DupArchiveHeaderU::readStandardHeaderField($archiveHandle, 'RPL');
// Skip the <RP>
fread($archiveHandle, 4);
$instance->relativePath = fread($archiveHandle, $instance->relativePathLength);
// Skip the </RP>
// fread($archiveHandle, 5);
// Skip the </D>
// fread($archiveHandle, 4);
// Skip the </RP> and the </D>
fread($archiveHandle, 9);
return $instance;
}
}

View File

@@ -0,0 +1,100 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\DupArchive\Headers;
use Exception;
/**
* File header
*/
class DupArchiveReaderFileHeader
{
public $fileSize = 0;
public $mtime = 0;
public $permissions = '';
public $hash = '';
public $relativePathLength = 0;
public $relativePath = '';
/**
* Class constructor
*/
protected function __construct()
{
// Prevent direct instantiation
}
/**
* Read header form archive
* delta = 84-22 = 62 bytes per file -> 20000 files -> 1.2MB larger
* <F><FS>x</FS><MT>x</<MT><FP>x</FP><HA>x</HA><RFPL>x</RFPL><RFP>x</RFP></F>
* # F#x#x#x#x#x#x!
*
* @param resource $archiveHandle archive resource
* @param boolean $skipContents if true skip contents
* @param boolean $skipMarker if true skip marker
*
* @return static
*/
public static function readFromArchive($archiveHandle, $skipContents = false, $skipMarker = false)
{
// RSR TODO Read header from archive handle and populate members
// TODO: return null if end of archive or throw exception if can read something but its not a file header
$instance = new static();
if (!$skipMarker) {
$marker = @fread($archiveHandle, 3);
if ($marker === false) {
if (feof($archiveHandle)) {
return false;
} else {
throw new Exception('Error reading file header');
}
}
if ($marker != '<F>') {
throw new Exception("Invalid file header marker found [{$marker}] : location " . ftell($archiveHandle));
}
}
$instance->fileSize = DupArchiveHeaderU::readStandardHeaderField($archiveHandle, 'FS');
$instance->mtime = DupArchiveHeaderU::readStandardHeaderField($archiveHandle, 'MT');
$instance->permissions = DupArchiveHeaderU::readStandardHeaderField($archiveHandle, 'P');
$instance->hash = DupArchiveHeaderU::readStandardHeaderField($archiveHandle, 'HA');
$instance->relativePathLength = DupArchiveHeaderU::readStandardHeaderField($archiveHandle, 'RPL');
// Skip <RP>
fread($archiveHandle, 4);
$instance->relativePath = fread($archiveHandle, $instance->relativePathLength);
// Skip </RP>
// fread($archiveHandle, 5);
// Skip the </F>
// fread($archiveHandle, 4);
// Skip the </RP> and the </F>
fread($archiveHandle, 9);
if ($skipContents && ($instance->fileSize > 0)) {
$dataSize = 0;
$moreGlobs = true;
while ($moreGlobs) {
$globHeader = DupArchiveReaderGlobHeader::readFromArchive($archiveHandle, true);
$dataSize += $globHeader->originalSize;
$moreGlobs = ($dataSize < $instance->fileSize);
}
}
return $instance;
}
}

View File

@@ -0,0 +1,88 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\DupArchive\Headers;
use Exception;
/**
* Dup archive glob header
*
* Format
* #C#{$originalSize}#{$storedSize}!
*/
class DupArchiveReaderGlobHeader
{
// public $marker;
public $originalSize;
public $storedSize;
public $hash;
/**
* Class constructor
*/
public function __construct()
{
}
/**
* Read chunk file header from archive
*
* @param resource $archiveHandle archive file resource
* @param bool $skipGlob if true skip glob content
*
* @return static
*/
public static function readFromArchive($archiveHandle, $skipGlob = false)
{
$instance = new static();
$startElement = fread($archiveHandle, 3);
//if ($marker != '?G#') {
if ($startElement !== '<G>') {
throw new Exception("Invalid glob header marker found {$startElement}. location:" . ftell($archiveHandle));
}
$instance->originalSize = DupArchiveHeaderU::readStandardHeaderField($archiveHandle, 'OS');
$instance->storedSize = DupArchiveHeaderU::readStandardHeaderField($archiveHandle, 'SS');
$instance->hash = DupArchiveHeaderU::readStandardHeaderField($archiveHandle, 'HA');
// Skip the </G>
fread($archiveHandle, 4);
if ($skipGlob) {
if (fseek($archiveHandle, $instance->storedSize, SEEK_CUR) === -1) {
throw new Exception("Can't fseek when skipping glob at location:" . ftell($archiveHandle));
}
}
return $instance;
}
/**
* Get glob content from header
*
* @param resource $archiveHandle archive hadler
* @param self $header chunk glob header
* @param bool $isCompressed true if is compressed
*
* @return string
*/
public static function readContent($archiveHandle, self $header, $isCompressed)
{
if ($header->storedSize == 0) {
return 0;
}
if (($globContents = fread($archiveHandle, $header->storedSize)) === false) {
throw new Exception("Error reading glob content");
}
return ($isCompressed ? gzinflate($globContents) : $globContents);
}
}

View File

@@ -0,0 +1,55 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\DupArchive\Headers;
use Exception;
/**
* Dup archive read header
*
* Format: #A#{version:5}#{isCompressed}!
*/
class DupArchiveReaderHeader
{
/** @var string */
protected $version;
/** @var bool */
public $isCompressed;
/**
* Class Contructor
*/
protected function __construct()
{
// Prevent instantiation
}
/**
* Get header from archive
*
* @param resource $archiveHandle archive resource
*
* @return static
*/
public static function readFromArchive($archiveHandle)
{
$instance = new static();
$startElement = fgets($archiveHandle, 4);
if ($startElement != '<A>') {
throw new Exception("Invalid archive header marker found {$startElement}");
}
$instance->version = DupArchiveHeaderU::readStandardHeaderField($archiveHandle, 'V');
$instance->isCompressed = filter_var(DupArchiveHeaderU::readStandardHeaderField($archiveHandle, 'C'), FILTER_VALIDATE_BOOLEAN);
// Skip the </A>
fgets($archiveHandle, 5);
return $instance;
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Duplicator\Libs\DupArchive\Info;
class DupArchiveExpanderInfo
{
public $archiveHandle = null;
public $currentFileHeader = null;
public $destDirectory = null;
public $directoryWriteCount = 0;
public $fileWriteCount = 0;
public $isCompressed = false;
public $enableWrite = false;
/**
* Get dest path
*
* @return string
*/
public function getCurrentDestFilePath()
{
if ($this->destDirectory != null) {
return "{$this->destDirectory}/{$this->currentFileHeader->relativePath}";
} else {
return null;
}
}
}

View File

@@ -0,0 +1,25 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\DupArchive\Info;
class DupArchiveInfo
{
public $archiveHeader;
public $fileHeaders;
public $directoryHeaders;
/**
* Class constructor
*/
public function __construct()
{
$this->fileHeaders = array();
$this->directoryHeaders = array();
}
}

View File

@@ -0,0 +1,45 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\DupArchive\Processors;
use Duplicator\Libs\DupArchive\Headers\DupArchiveDirectoryHeader;
use Duplicator\Libs\DupArchive\States\DupArchiveCreateState;
use Duplicator\Libs\Snap\SnapIO;
class DupArchiveDirectoryProcessor
{
/**
* Undocumented function
*
* @param DupArchiveCreateState $createState create state
* @param resource $archiveHandle archive resource
* @param string $sourceDirectoryPath source directory path
* @param string $relativeDirectoryPath relative dirctory path
*
* @return void
*/
public static function writeDirectoryToArchive(
DupArchiveCreateState $createState,
$archiveHandle,
$sourceDirectoryPath,
$relativeDirectoryPath
) {
$directoryHeader = new DupArchiveDirectoryHeader();
$directoryHeader->permissions = substr(sprintf('%o', fileperms($sourceDirectoryPath)), -4);
$directoryHeader->mtime = SnapIO::filemtime($sourceDirectoryPath);
$directoryHeader->relativePath = $relativeDirectoryPath;
$directoryHeader->relativePathLength = strlen($directoryHeader->relativePath);
$directoryHeader->writeToArchive($archiveHandle);
// Just increment this here - the actual state save is on the outside after timeout or completion of all directories
$createState->currentDirectoryIndex++;
}
}

View File

@@ -0,0 +1,548 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\DupArchive\Processors;
use Duplicator\Libs\DupArchive\DupArchiveEngine;
use Duplicator\Libs\DupArchive\Headers\DupArchiveDirectoryHeader;
use Duplicator\Libs\DupArchive\Headers\DupArchiveFileHeader;
use Duplicator\Libs\DupArchive\Headers\DupArchiveGlobHeader;
use Duplicator\Libs\DupArchive\Processors\DupArchiveProcessingFailure;
use Duplicator\Libs\DupArchive\States\DupArchiveCreateState;
use Duplicator\Libs\DupArchive\States\DupArchiveExpandState;
use Duplicator\Libs\DupArchive\Utils\DupArchiveUtil;
use Duplicator\Libs\Snap\SnapIO;
use Exception;
/**
* Dup archive file processor
*/
class DupArchiveFileProcessor
{
protected static $newFilePathCallback = null;
/**
* Set new file callback
*
* @param callable $callback callback function
*
* @return bool
*/
public static function setNewFilePathCallback($callback)
{
if (!is_callable($callback)) {
self::$newFilePathCallback = null;
return false;
}
self::$newFilePathCallback = $callback;
return true;
}
/**
* get file from relatei path
*
* @param string $basePath base path
* @param string $relativePath relative path
*
* @return string
*/
protected static function getNewFilePath($basePath, $relativePath)
{
if (is_null(self::$newFilePathCallback)) {
return $basePath . '/' . $relativePath;
} else {
return call_user_func_array(self::$newFilePathCallback, array($relativePath));
}
}
/**
* Write file to archive
*
* @param DupArchiveCreateState $createState dup archive create state
* @param resource $archiveHandle archive resource
* @param string $sourceFilepath source file path
* @param string $relativeFilePath relative file path
*
* @return void
*/
public static function writeFilePortionToArchive(
DupArchiveCreateState $createState,
$archiveHandle,
$sourceFilepath,
$relativeFilePath
) {
DupArchiveUtil::tlog("writeFileToArchive for {$sourceFilepath}");
// switching to straight call for speed
$sourceHandle = @fopen($sourceFilepath, 'rb');
if (!is_resource($sourceHandle)) {
$createState->archiveOffset = SnapIO::ftell($archiveHandle);
$createState->currentFileIndex++;
$createState->currentFileOffset = 0;
$createState->skippedFileCount++;
$createState->addFailure(DupArchiveProcessingFailure::TYPE_FILE, $sourceFilepath, "Couldn't open $sourceFilepath", false);
return;
}
if ($createState->currentFileOffset > 0) {
SnapIO::fseek($sourceHandle, $createState->currentFileOffset);
} else {
$fileHeader = DupArchiveFileHeader::createFromFile($sourceFilepath, $relativeFilePath);
$fileHeader->writeToArchive($archiveHandle);
}
$sourceFileSize = filesize($sourceFilepath);
$moreFileDataToProcess = true;
while ((!$createState->timedOut()) && $moreFileDataToProcess) {
if ($createState->throttleDelayInUs !== 0) {
usleep($createState->throttleDelayInUs);
}
$moreFileDataToProcess = self::appendGlobToArchive($createState, $archiveHandle, $sourceHandle, $sourceFilepath, $sourceFileSize);
$createState->archiveOffset = SnapIO::ftell($archiveHandle);
if ($moreFileDataToProcess) {
$createState->currentFileOffset += $createState->globSize;
} else {
$createState->currentFileIndex++;
$createState->currentFileOffset = 0;
}
// Only writing state after full group of files have been written - less reliable but more efficient
// $createState->save();
}
SnapIO::fclose($sourceHandle);
}
/**
* Write file to archive from source
*
* @param DupArchiveCreateState $createState dup archive create state
* @param resource $archiveHandle archive resource
* @param string $src source string
* @param string $relativeFilePath relative file path
* @param int $forceSize if 0 size is auto of content is filled of \0 char to size
*
* @return void
*/
public static function writeFileSrcToArchive(
DupArchiveCreateState $createState,
$archiveHandle,
$src,
$relativeFilePath,
$forceSize = 0
) {
DupArchiveUtil::tlog("writeFileSrcToArchive");
$fileHeader = DupArchiveFileHeader::createFromSrc($src, $relativeFilePath, $forceSize);
$fileHeader->writeToArchive($archiveHandle);
self::appendFileSrcToArchive($createState, $archiveHandle, $src, $forceSize);
$createState->currentFileIndex++;
$createState->currentFileOffset = 0;
$createState->archiveOffset = SnapIO::ftell($archiveHandle);
}
/**
* Expand du archive
*
* Assumption is that this is called at the beginning of a glob header since file header already writtern
*
* @param DupArchiveExpandState $expandState expand state
* @param resource $archiveHandle archive resource
*
* @return bool true on success
*/
public static function writeToFile(DupArchiveExpandState $expandState, $archiveHandle)
{
if (isset($expandState->fileRenames[$expandState->currentFileHeader->relativePath])) {
$destFilepath = $expandState->fileRenames[$expandState->currentFileHeader->relativePath];
} else {
$destFilepath = self::getNewFilePath($expandState->basePath, $expandState->currentFileHeader->relativePath);
}
$parentDir = dirname($destFilepath);
$moreGlobstoProcess = true;
SnapIO::dirWriteCheckOrMkdir($parentDir, 'u+rwx', true);
if ($expandState->currentFileHeader->fileSize > 0) {
if ($expandState->currentFileOffset > 0) {
$destFileHandle = SnapIO::fopen($destFilepath, 'r+b');
SnapIO::fseek($destFileHandle, $expandState->currentFileOffset);
} else {
$destFileHandle = SnapIO::fopen($destFilepath, 'w+b');
}
while (!$expandState->timedOut()) {
$moreGlobstoProcess = $expandState->currentFileOffset < $expandState->currentFileHeader->fileSize;
if ($moreGlobstoProcess) {
if ($expandState->throttleDelayInUs !== 0) {
usleep($expandState->throttleDelayInUs);
}
self::appendGlobToFile($expandState, $archiveHandle, $destFileHandle, $destFilepath);
$expandState->currentFileOffset = ftell($destFileHandle);
$expandState->archiveOffset = SnapIO::ftell($archiveHandle);
$moreGlobstoProcess = $expandState->currentFileOffset < $expandState->currentFileHeader->fileSize;
if (!$moreGlobstoProcess) {
break;
}
} else {
// rsr todo record fclose error
@fclose($destFileHandle);
$destFileHandle = null;
if ($expandState->validationType == DupArchiveExpandState::VALIDATION_FULL) {
self::validateExpandedFile($expandState);
}
break;
}
}
DupArchiveUtil::tlog('Out of glob loop');
if ($destFileHandle != null) {
// rsr todo record file close error
@fclose($destFileHandle);
$destFileHandle = null;
}
if (!$moreGlobstoProcess && $expandState->validateOnly && ($expandState->validationType == DupArchiveExpandState::VALIDATION_FULL)) {
if (!is_writable($destFilepath)) {
SnapIO::chmod($destFilepath, 'u+rw');
}
if (@unlink($destFilepath) === false) {
// $expandState->addFailure(DupArchiveFailureTypes::File, $destFilepath, "Couldn't delete {$destFilepath} during validation", false);
// TODO: Have to know how to handle this - want to report it but dont want to mess up validation -
// some non critical errors could be important to validation
}
}
} else {
// 0 length file so just touch it
$moreGlobstoProcess = false;
if (file_exists($destFilepath)) {
@unlink($destFilepath);
}
if (touch($destFilepath) === false) {
throw new Exception("Couldn't create {$destFilepath}");
}
}
if (!$moreGlobstoProcess) {
self::setFileMode($expandState, $destFilepath);
self::setFileTimes($expandState, $destFilepath);
DupArchiveUtil::tlog('No more globs to process');
$expandState->fileWriteCount++;
$expandState->resetForFile();
}
return !$moreGlobstoProcess;
}
/**
* Create directory
*
* @param DupArchiveExpandState $expandState expand state
* @param DupArchiveDirectoryHeader $directoryHeader directory header
*
* @return boolean
*/
public static function createDirectory(DupArchiveExpandState $expandState, DupArchiveDirectoryHeader $directoryHeader)
{
/* @var $expandState DupArchiveExpandState */
$destDirPath = self::getNewFilePath($expandState->basePath, $directoryHeader->relativePath);
$mode = $directoryHeader->permissions;
if ($expandState->directoryModeOverride != -1) {
$mode = $expandState->directoryModeOverride;
}
if (!SnapIO::dirWriteCheckOrMkdir($destDirPath, $mode, true)) {
$error_message = "Unable to create directory $destDirPath";
$expandState->addFailure(DupArchiveProcessingFailure::TYPE_DIRECTORY, $directoryHeader->relativePath, $error_message, false);
DupArchiveUtil::tlog($error_message);
return false;
} else {
return true;
}
}
/**
* Set file mode if is enabled
*
* @param DupArchiveExpandState $expandState dup expand state
* @param string $filePath file path
*
* @return bool
*/
public static function setFileMode(DupArchiveExpandState $expandState, $filePath)
{
if ($expandState->fileModeOverride === -1) {
return;
}
return SnapIO::chmod($filePath, $expandState->fileModeOverride);
}
/**
* Set original file times if enabled
*
* @param DupArchiveExpandState $expandState dup expand state
* @param string $filePath File path
*
* @return bool true if success, false otherwise
*/
protected static function setFileTimes(DupArchiveExpandState $expandState, $filePath)
{
if (!$expandState->keepFileTime) {
return true;
}
if (!file_exists($filePath)) {
return false;
}
return touch($filePath, $expandState->currentFileHeader->mtime);
}
/**
* Validate file entry
*
* @param DupArchiveExpandState $expandState dup expand state
* @param resource $archiveHandle dup archive resource
*
* @return bool
*/
public static function standardValidateFileEntry(DupArchiveExpandState $expandState, $archiveHandle)
{
$moreGlobstoProcess = $expandState->currentFileOffset < $expandState->currentFileHeader->fileSize;
if (!$moreGlobstoProcess) {
// Not a 'real' write but indicates that we actually did fully process a file in the archive
$expandState->fileWriteCount++;
} else {
while ((!$expandState->timedOut()) && $moreGlobstoProcess) {
// Read in the glob header but leave the pointer at the payload
$globHeader = DupArchiveGlobHeader::readFromArchive($archiveHandle, false);
$globContents = fread($archiveHandle, $globHeader->storedSize);
if ($globContents === false) {
throw new Exception("Error reading glob from archive");
}
$hash = hash('crc32b', $globContents);
if ($hash != $globHeader->hash) {
$expandState->addFailure(
DupArchiveProcessingFailure::TYPE_FILE,
$expandState->currentFileHeader->relativePath,
'Hash mismatch on DupArchive file entry',
true
);
DupArchiveUtil::tlog("Glob hash mismatch during standard check of {$expandState->currentFileHeader->relativePath}");
} else {
// DupArchiveUtil::tlog("Glob MD5 passes");
}
$expandState->currentFileOffset += $globHeader->originalSize;
$expandState->archiveOffset = SnapIO::ftell($archiveHandle);
$moreGlobstoProcess = $expandState->currentFileOffset < $expandState->currentFileHeader->fileSize;
if (!$moreGlobstoProcess) {
$expandState->fileWriteCount++;
$expandState->resetForFile();
}
}
}
return !$moreGlobstoProcess;
}
/**
* Validate file
*
* @param DupArchiveExpandState $expandState dup expand state
*
* @return void
*/
private static function validateExpandedFile(DupArchiveExpandState $expandState)
{
/* @var $expandState DupArchiveExpandState */
$destFilepath = self::getNewFilePath($expandState->basePath, $expandState->currentFileHeader->relativePath);
if ($expandState->currentFileHeader->hash !== '00000000000000000000000000000000') {
$hash = hash_file('crc32b', $destFilepath);
if ($hash !== $expandState->currentFileHeader->hash) {
$expandState->addFailure(DupArchiveProcessingFailure::TYPE_FILE, $destFilepath, "MD5 mismatch for {$destFilepath}", false);
} else {
DupArchiveUtil::tlog('MD5 Match for ' . $destFilepath);
}
} else {
DupArchiveUtil::tlog('MD5 non match is 0\'s');
}
}
/**
* Append file to archive
*
* @param DupArchiveCreateState $createState create state
* @param resource $archiveHandle archive resource
* @param resource $sourceFilehandle file resource
* @param string $sourceFilepath file path
* @param int $fileSize file size
*
* @return bool true if more file remaning
*/
private static function appendGlobToArchive(
DupArchiveCreateState $createState,
$archiveHandle,
$sourceFilehandle,
$sourceFilepath,
$fileSize
) {
DupArchiveUtil::tlog("Appending file glob to archive for file {$sourceFilepath} at file offset {$createState->currentFileOffset}");
if ($fileSize == 0) {
return false;
}
$fileSize -= $createState->currentFileOffset;
$globContents = @fread($sourceFilehandle, $createState->globSize);
if ($globContents === false) {
throw new Exception("Error reading $sourceFilepath");
}
$originalSize = strlen($globContents);
if ($createState->isCompressed) {
$globContents = gzdeflate($globContents, 2); // 2 chosen as best compromise between speed and size
$storeSize = strlen($globContents);
} else {
$storeSize = $originalSize;
}
$globHeader = new DupArchiveGlobHeader();
$globHeader->originalSize = $originalSize;
$globHeader->storedSize = $storeSize;
$globHeader->hash = hash('crc32b', $globContents);
$globHeader->writeToArchive($archiveHandle);
if (@fwrite($archiveHandle, $globContents) === false) {
// Considered fatal since we should always be able to write to the archive -
// plus the header has already been written (could back this out later though)
throw new Exception(
"Error writing $sourceFilepath to archive. Ensure site still hasn't run out of space.",
DupArchiveEngine::EXCEPTION_FATAL
);
}
$fileSizeRemaining = $fileSize - $createState->globSize;
$moreFileRemaining = $fileSizeRemaining > 0;
return $moreFileRemaining;
}
/**
* Append file in dup archvie from source string
*
* @param DupArchiveCreateState $createState create state
* @param resource $archiveHandle archive handle
* @param string $src source to add
* @param int $forceSize if 0 size is auto of content is filled of \0 char to size
*
* @return bool
*/
private static function appendFileSrcToArchive(
DupArchiveCreateState $createState,
$archiveHandle,
$src,
$forceSize = 0
) {
DupArchiveUtil::tlog("Appending file glob to archive from src");
if (($originalSize = strlen($src)) == 0 && $forceSize == 0) {
return false;
}
if ($forceSize == 0 && $createState->isCompressed) {
$src = gzdeflate($src, 2); // 2 chosen as best compromise between speed and size
$storeSize = strlen($src);
} else {
$storeSize = $originalSize;
}
if ($forceSize > 0 && $storeSize < $forceSize) {
$charsToAdd = $forceSize - $storeSize;
$src .= str_repeat("\0", $charsToAdd);
$storeSize = $forceSize;
}
$globHeader = new DupArchiveGlobHeader();
$globHeader->originalSize = $originalSize;
$globHeader->storedSize = $storeSize;
$globHeader->hash = hash('crc32b', $src);
$globHeader->writeToArchive($archiveHandle);
if (SnapIO::fwriteChunked($archiveHandle, $src) === false) {
// Considered fatal since we should always be able to write to the archive -
// plus the header has already been written (could back this out later though)
throw new Exception(
"Error writing SRC to archive. Ensure site still hasn't run out of space.",
DupArchiveEngine::EXCEPTION_FATAL
);
}
return true;
}
/**
* Extract file from dup archive
* Assumption is that archive handle points to a glob header on this call
*
* @param DupArchiveExpandState $expandState dup archive expand state
* @param resource $archiveHandle archvie resource
* @param resource $destFileHandle file resource
* @param string $destFilePath file path
*
* @return void
*/
private static function appendGlobToFile(
DupArchiveExpandState $expandState,
$archiveHandle,
$destFileHandle,
$destFilePath
) {
DupArchiveUtil::tlog('Appending file glob to file ' . $destFilePath . ' at file offset ' . $expandState->currentFileOffset);
// Read in the glob header but leave the pointer at the payload
$globHeader = DupArchiveGlobHeader::readFromArchive($archiveHandle, false);
if (($globContents = DupArchiveGlobHeader::readContent($archiveHandle, $globHeader, $expandState->archiveHeader->isCompressed)) === false) {
throw new Exception("Error reading glob from $destFilePath");
}
if (@fwrite($destFileHandle, $globContents) === false) {
throw new Exception("Error writing glob to $destFilePath");
} else {
DupArchiveUtil::tlog('Successfully wrote glob');
}
}
}

View File

@@ -0,0 +1,24 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\DupArchive\Processors;
/**
*Failure class
*/
class DupArchiveProcessingFailure
{
const TYPE_UNKNOWN = 0;
const TYPE_FILE = 1;
const TYPE_DIRECTORY = 2;
public $type = self::TYPE_UNKNOWN;
public $description = '';
public $subject = '';
public $isCritical = false;
}

View File

@@ -0,0 +1,40 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\DupArchive\States;
/**
* Dup archive create state
*/
abstract class DupArchiveCreateState extends DupArchiveStateBase
{
const DEFAULT_GLOB_SIZE = 1048576;
public $basepathLength = 0;
public $currentDirectoryIndex = -1;
public $currentFileIndex = -1;
public $globSize = self::DEFAULT_GLOB_SIZE;
public $newBasePath = null;
public $skippedFileCount = 0;
public $skippedDirectoryCount = 0;
/**
* Class constructor
*/
public function __construct()
{
parent::__construct();
}
/**
* State save
*
* @return void
*/
abstract public function save();
}

View File

@@ -0,0 +1,63 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\DupArchive\States;
use Duplicator\Libs\DupArchive\Headers\DupArchiveFileHeader;
use Duplicator\Libs\DupArchive\Headers\DupArchiveHeader;
/**
* Dup archive expand state
*/
abstract class DupArchiveExpandState extends DupArchiveStateBase
{
const VALIDATION_NONE = 0;
const VALIDATION_STANDARD = 1;
const VALIDATION_FULL = 2;
/** @var DupArchiveHeader */
public $archiveHeader = null;
/** @var DupArchiveFileHeader */
public $currentFileHeader = null;
public $validateOnly = false;
public $validationType = self::VALIDATION_STANDARD;
public $fileWriteCount = 0;
public $directoryWriteCount = 0;
public $expectedFileCount = -1;
public $expectedDirectoryCount = -1;
public $filteredDirectories = array();
public $excludedDirWithoutChilds = array();
public $filteredFiles = array();
/** @var string[] relative path list to inclue files, overwrite filters */
public $includedFiles = array();
/** @var string[] relativePath => fullNewPath */
public $fileRenames = array();
public $directoryModeOverride = -1;
public $fileModeOverride = -1;
public $lastHeaderOffset = -1;
/** @var bool */
public $keepFileTime = false;
/**
* Reset state for file
*
* @return void
*/
public function resetForFile()
{
$this->currentFileHeader = null;
$this->currentFileOffset = 0;
}
/**
* save expand state
*
* @return void
*/
abstract public function save();
}

View File

@@ -0,0 +1,34 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\DupArchive\States;
/**
* Simple create state
*/
class DupArchiveSimpleCreateState extends DupArchiveCreateState
{
/**
* Class constructor
*/
public function __construct()
{
$this->currentDirectoryIndex = 0;
$this->currentFileIndex = 0;
$this->currentFileOffset = 0;
}
/**
* Save state
*
* @return void
*/
public function save()
{
}
}

View File

@@ -0,0 +1,31 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\DupArchive\States;
/**
* Simple expand state
*/
class DupArchiveSimpleExpandState extends DupArchiveExpandState
{
/**
* Class constructor
*/
public function __construct()
{
}
/**
* save function
*
* @return void
*/
public function save()
{
}
}

View File

@@ -0,0 +1,170 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\DupArchive\States;
use Duplicator\Libs\DupArchive\Processors\DupArchiveProcessingFailure;
/**
* Dup archive state base
*/
abstract class DupArchiveStateBase
{
const MAX_FAILURE = 1000;
public $basePath = '';
public $archivePath = '';
public $isCompressed = false;
public $currentFileOffset = -1;
public $archiveOffset = -1;
public $timeSliceInSecs = -1;
public $working = false;
/** @var DupArchiveProcessingFailure[] */
public $failures = array();
public $failureCount = 0;
public $startTimestamp = -1;
public $throttleDelayInUs = 0;
public $timeoutTimestamp = -1;
public $timerEnabled = true;
public $isRobust = false;
/**
* Class constructor
*/
public function __construct()
{
}
/**
* Check if is present a critical failure
*
* @return boolean
*/
public function isCriticalFailurePresent()
{
if (count($this->failures) > 0) {
foreach ($this->failures as $failure) {
if ($failure->isCritical) {
return true;
}
}
}
return false;
}
/**
* Che failure summary
*
* @param boolean $includeCritical include critical failures
* @param boolean $includeWarnings include warnings failures
*
* @return string
*/
public function getFailureSummary($includeCritical = true, $includeWarnings = false)
{
if (count($this->failures) > 0) {
$message = '';
foreach ($this->failures as $failure) {
if ($includeCritical || !$failure->isCritical) {
$message .= "\n" . $this->getFailureString($failure);
}
}
return $message;
} else {
if ($includeCritical) {
if ($includeWarnings) {
return 'No errors or warnings.';
} else {
return 'No errors.';
}
} else {
return 'No warnings.';
}
}
}
/**
* Return failure string from item
*
* @param DupArchiveProcessingFailure $failure failure item
*
* @return string
*/
public function getFailureString(DupArchiveProcessingFailure $failure)
{
$s = '';
if ($failure->isCritical) {
$s = 'CRITICAL: ';
}
return "{$s}{$failure->subject} : {$failure->description}";
}
/**
* Add failure item
*
* @param int $type failure type enum
* @param string $subject failure subject
* @param string $description failure description
* @param boolean $isCritical true if is critical
*
* @return DupArchiveProcessingFailure
*/
public function addFailure($type, $subject, $description, $isCritical = true)
{
$this->failureCount++;
if ($this->failureCount > self::MAX_FAILURE) {
return false;
}
$failure = new DupArchiveProcessingFailure();
$failure->type = $type;
$failure->subject = $subject;
$failure->description = $description;
$failure->isCritical = $isCritical;
$this->failures[] = $failure;
return $failure;
}
/**
* Set start time
*
* @return void
*/
public function startTimer()
{
if ($this->timerEnabled) {
$this->timeoutTimestamp = time() + $this->timeSliceInSecs;
}
}
/**
* Check if is timeout
*
* @return bool
*/
public function timedOut()
{
if ($this->timerEnabled) {
if ($this->timeoutTimestamp != -1) {
return time() >= $this->timeoutTimestamp;
} else {
return false;
}
} else {
return false;
}
}
}

View File

@@ -0,0 +1,96 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\DupArchive\Utils;
use Duplicator\Libs\Snap\SnapJson;
use Exception;
use stdClass;
/**
* Description of class
*
* @author Robert
*/
class DupArchiveScanUtil
{
/**
* Get scan
*
* @param string $scanFilepath scan file path
*
* @return void
*/
public static function getScan($scanFilepath)
{
DupArchiveUtil::tlog("Getting scen");
$scan_handle = fopen($scanFilepath, 'r');
if ($scan_handle === false) {
throw new Exception("Can't open {$scanFilepath}");
}
$scan_file = fread($scan_handle, filesize($scanFilepath));
if ($scan_file === false) {
throw new Exception("Can't read from {$scanFilepath}");
}
$scan = json_decode($scan_file);
if (!$scan) {
throw new Exception("Error decoding scan file");
}
fclose($scan_handle);
return $scan;
}
/**
* Get scan object
*
* @param string $sourceDirectory folder to scan
*
* @return stdClass
*/
public static function createScanObject($sourceDirectory)
{
$scan = new stdClass();
$scan->Dirs = DupArchiveUtil::expandDirectories($sourceDirectory, true);
$scan->Files = DupArchiveUtil::expandFiles($sourceDirectory, true);
return $scan;
}
/**
* Scan folder and add result to scan file
*
* @param string $scanFilepath scan file
* @param string $sourceDirectory folder to scan
*
* @return void
*/
public static function createScan($scanFilepath, $sourceDirectory)
{
DupArchiveUtil::tlog("Creating scan");
$scan = self::createScanObject($sourceDirectory);
$scan_handle = fopen($scanFilepath, 'w');
if ($scan_handle === false) {
echo "Couldn't create scan file";
die();
}
$jsn = SnapJson::jsonEncode($scan);
fwrite($scan_handle, $jsn);
return $scan;
}
}

View File

@@ -0,0 +1,165 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\DupArchive\Utils;
use Duplicator\Libs\Snap\SnapUtil;
class DupArchiveUtil
{
public static $TRACE_ON = false; //rodo rework this
public static $logger = null;
/**
* get file list
*
* @param string $base_dir folder to check
* @param bool $recurse true for recursive scan
*
* @return string[]
*/
public static function expandFiles($base_dir, $recurse)
{
$files = array();
foreach (scandir($base_dir) as $file) {
if (($file == '.') || ($file == '..')) {
continue;
}
$file = "{$base_dir}/{$file}";
if (is_file($file)) {
$files [] = $file;
} elseif (is_dir($file) && $recurse) {
$files = array_merge($files, self::expandFiles($file, $recurse));
}
}
return $files;
}
/**
* get folder list
*
* @param string $base_dir folder to check
* @param bool $recurse true for recursive scan
*
* @return string[]
*/
public static function expandDirectories($base_dir, $recurse)
{
$directories = array();
foreach (scandir($base_dir) as $candidate) {
if (($candidate == '.') || ($candidate == '..')) {
continue;
}
$candidate = "{$base_dir}/{$candidate}";
if (is_dir($candidate)) {
$directories[] = $candidate;
if ($recurse) {
$directories = array_merge($directories, self::expandDirectories($candidate, $recurse));
}
}
}
return $directories;
}
/**
* Write $s in log
*
* @param string $s log string
* @param boolean $flush if true flosh name
* @param string $callingFunctionName function has called log
*
* @return void
*/
public static function log($s, $flush = false, $callingFunctionName = null)
{
if (self::$logger != null) {
if ($callingFunctionName === null) {
$callingFunctionName = SnapUtil::getCallingFunctionName();
}
self::$logger->log($s, $flush, $callingFunctionName);
} else {
// throw new Exception('Logging object not initialized');
}
}
/**
* Write trace log
*
* @param string $s log string
* @param boolean $flush if true flosh name
* @param string $callingFunctionName function has called log
*
* @return void
*/
public static function tlog($s, $flush = false, $callingFunctionName = null)
{
if (self::$TRACE_ON) {
if ($callingFunctionName === null) {
$callingFunctionName = SnapUtil::getCallingFunctionName();
}
self::log("####{$s}", $flush, $callingFunctionName);
}
}
/**
* Write object in trace log
*
* @param string $s log string
* @param mixed $o value to write in log
* @param boolean $flush if true flosh name
* @param string $callingFunctionName function has called log
*
* @return void
*/
public static function tlogObject($s, $o, $flush = false, $callingFunctionName = null)
{
if (is_object($o)) {
$o = get_object_vars($o);
}
$ostring = print_r($o, true);
if ($callingFunctionName === null) {
$callingFunctionName = SnapUtil::getCallingFunctionName();
}
self::tlog($s, $flush, $callingFunctionName);
self::tlog($ostring, $flush, $callingFunctionName);
}
/**
* Write object in log
*
* @param string $s log string
* @param mixed $o value to write in log
* @param boolean $flush if true flosh name
* @param string $callingFunctionName function has called log
*
* @return void
*/
public static function logObject($s, $o, $flush = false, $callingFunctionName = null)
{
$ostring = print_r($o, true);
if ($callingFunctionName === null) {
$callingFunctionName = SnapUtil::getCallingFunctionName();
}
self::log($s, $flush, $callingFunctionName);
self::log($ostring, $flush, $callingFunctionName);
}
}

View File

@@ -0,0 +1,118 @@
<?php
/**
* @package Duplicator
*
* phpcs:disable PSR1.Files.SideEffects.FoundWithSymbols
* phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
*/
namespace Duplicator\Libs\OneClickUpgrade;
use WP_Upgrader_Skin;
defined('ABSPATH') || exit;
/**
* Silent upgrader skin for one-click upgrade
*
* @since 1.5.13
*/
class UpgraderSkin extends WP_Upgrader_Skin
{
/**
* Primary class constructor.
*
* @since 1.5.13
*
* @param array $args Empty array of args (we will use defaults).
*/
public function __construct($args = array())
{
parent::__construct($args);
}
/**
* Set the upgrader object and store it as a property in the parent class.
*
* @since 1.5.13
*
* @param object $upgrader The upgrader object (passed by reference).
*
* @return void
*/
public function set_upgrader(&$upgrader)
{
if (is_object($upgrader)) {
$this->upgrader =& $upgrader;
}
}
/**
* Set the upgrader result and store it as a property in the parent class.
*
* @since 1.5.13
*
* @param object $result The result of the install process.
*
* @return void
*/
public function set_result($result)
{
$this->result = $result;
}
/**
* Empty out the header of its HTML content and only check to see if it has
* been performed or not.
*
* @since 1.5.13
*
* @return void
*/
public function header()
{
}
/**
* Empty out the footer of its HTML contents.
*
* @since 1.5.13
*
* @return void
*/
public function footer()
{
}
/**
* Instead of outputting HTML for errors, send proper WordPress AJAX error response.
*
* @since 1.5.13
*
* @param array $errors Array of errors with the install process.
*
* @return void
*/
public function error($errors)
{
if (!empty($errors)) {
wp_send_json_error(array('message' => esc_html__('There was an error installing the upgrade. Please try again.', 'duplicator')));
}
}
/**
* Empty out the feedback method to prevent outputting HTML strings as the install
* is progressing.
*
* @since 1.5.13
*
* @param string $string The feedback string.
* @param mixed ...$args Additional arguments.
*
* @return void
*/
public function feedback($string, ...$args)
{
}
}

View File

@@ -0,0 +1,161 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Libs\Snap;
use Exception;
/**
* Functionality to check
*/
class FunctionalityCheck
{
const TYPE_FUNCTION = 1;
const TYPE_CLASS = 2;
/** @var int Enum type */
protected $type = 0;
/** @var string item key to test */
protected $itemKey = '';
/** @var bool true if this item is required */
protected $required = false;
/** @var string link to documntation */
public $link = '';
/** @var string html troubleshoot */
public $troubleshoot = '';
/** @var ?callable if is set is called when check fail */
protected $failCallback = null;
/**
* Class contructor
*
* @param int $type Enum type
* @param string $key item key to test
* @param bool $required true if this item is required
* @param string $link link to documntation
* @param string $troubleshoot html troubleshoot
*/
public function __construct($type, $key, $required = false, $link = '', $troubleshoot = '')
{
switch ($type) {
case self::TYPE_FUNCTION:
case self::TYPE_CLASS:
$this->type = $type;
break;
default:
throw new Exception('Invalid item type');
}
if (strlen($key) == 0) {
throw new Exception('Key can\'t be empty');
}
$this->required = $required;
$this->itemKey = (string) $key;
$this->link = (string) $link;
$this->troubleshoot = (string) $troubleshoot;
}
/**
* Get the value of type
*
* @return int
*/
public function getType()
{
return $this->type;
}
/**
* Get the value of itemKey
*
* @return string
*/
public function getItemKey()
{
return $this->itemKey;
}
/**
* true if is required
*
* @return bool
*/
public function isRequired()
{
return $this->required;
}
/**
* Check if item exists
*
* @return bool
*/
public function check()
{
$result = false;
switch ($this->type) {
case self::TYPE_FUNCTION:
$result = function_exists($this->itemKey);
break;
case self::TYPE_CLASS:
$result = SnapUtil::classExists($this->itemKey);
break;
default:
throw new Exception('Invalid item type');
}
if ($result == false && is_callable($this->failCallback)) {
call_user_func($this->failCallback, $this);
}
return $result;
}
/**
* Set the value of failCallback
*
* @param callable $failCallback fail callback function
*
* @return void
*/
public function setFailCallback($failCallback)
{
$this->failCallback = $failCallback;
}
/**
* Check all Functionalities in list
*
* @param self[] $funcs Functionalities list
* @param bool $requiredOnly if true skip functs not required
* @param self[] $notPassList list of items that not have pass the test
*
* @return bool
*/
public static function checkList($funcs, $requiredOnly = false, &$notPassList = array())
{
if (!is_array($funcs)) {
throw new Exception('funcs must be an array');
}
$notPassList = array();
foreach ($funcs as $func) {
if ($requiredOnly && !$func->isRequired()) {
continue;
}
if ($func->check() === false) {
$notPassList[] = $func;
}
}
return (count($notPassList) === 0);
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*
* this file isn't under PSR4 autoloader standard
*/
if (!interface_exists('JsonSerializable')) {
if (!defined('WP_JSON_SERIALIZE_COMPATIBLE')) {
define('WP_JSON_SERIALIZE_COMPATIBLE', true);
}
/**
* JsonSerializable interface.
*
* Compatibility shim for PHP <5.4
*/
interface JsonSerializable // phpcs:ignore
{
/**
* Serialize object
*
* @return string
*/
public function jsonSerialize();
}
}

View File

@@ -0,0 +1,31 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\Snap\JsonSerialize;
// phpcs:disable
require_once(dirname(__DIR__) . '/JsonSerializable.php');
// phpcs:enable
/**
* Abstract class to extend in order to use the maximum potentialities of JsonSerialize
*/
// phpcs:ignore PHPCompatibility.Interfaces.NewInterfaces.jsonserializableFound
abstract class AbstractJsonSerializable extends AbstractJsonSerializeObjData implements \JsonSerializable
{
/**
* Prepared json serialized object
*
* @return mixed
*/
#[\ReturnTypeWillChange]
final public function jsonSerialize()
{
return self::objectToJsonData($this, 0, array());
}
}

View File

@@ -0,0 +1,205 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\Snap\JsonSerialize;
use Exception;
use ReflectionClass;
use ReflectionObject;
/**
* This calsse contains the logic that converts objects into values ready to be encoded in json
*/
abstract class AbstractJsonSerializeObjData
{
const CLASS_KEY_FOR_JSON_SERIALIZE = 'CL_-=_-=';
const JSON_SERIALIZE_SKIP_CLASS_NAME = 1073741824; // 30 bit mask
/**
* Convert object to array with private and protected proprieties.
* Private parent class proprieties aren't considered.
*
* @param object $obj obejct to serialize
* @param int $flags flags bitmask
* @param string[] $objParents objs parents unique objects hash list
*
* @return array
*/
final protected static function objectToJsonData($obj, $flags = 0, $objParents = array())
{
$reflect = new ReflectionObject($obj);
if (!($flags & self::JSON_SERIALIZE_SKIP_CLASS_NAME)) {
$result = array(self::CLASS_KEY_FOR_JSON_SERIALIZE => $reflect->name);
}
if (method_exists($obj, '__sleep')) {
$includeProps = $obj->__sleep();
if (!is_array($includeProps)) {
throw new Exception('__sleep method must return an array');
}
} else {
$includeProps = true;
}
// Get all props of current class but not props private of parent class and static props
foreach ($reflect->getProperties() as $prop) {
if ($prop->isStatic()) {
continue;
}
$propName = $prop->getName();
if ($includeProps !== true && !in_array($propName, $includeProps)) {
continue;
}
$prop->setAccessible(true);
$propValue = $prop->getValue($obj);
$result[$propName] = self::valueToJsonData($propValue, $flags, $objParents);
}
return $result;
}
/**
* Recursive parse values, all objects are transformed to array
*
* @param mixed $value valute to parse
* @param int $flags flags bitmask
* @param string[] $objParents objs parents unique hash ids
*
* @return mixed
*/
final public static function valueToJsonData($value, $flags = 0, $objParents = array())
{
switch (gettype($value)) {
case "boolean":
case "integer":
case "double":
case "string":
case "NULL":
return $value;
case "array":
$result = array();
foreach ($value as $key => $arrayVal) {
$result[$key] = self::valueToJsonData($arrayVal, $flags, $objParents);
}
return $result;
case "object":
$objHash = spl_object_hash($value);
if (in_array($objHash, $objParents)) {
// prevent infinite recursion loop
return null;
}
$objParents[] = $objHash;
return self::objectToJsonData($value, $flags, $objParents);
case "resource":
case "resource (closed)":
case "unknown type":
default:
return null;
}
}
/**
* Return value from json decoded data
*
* @param mixed $value json decoded data
*
* @return mixed
*/
final protected static function jsonDataToValue($value)
{
switch (gettype($value)) {
case 'array':
if (($newClassName = self::getClassFromArray($value)) === false) {
$result = array();
foreach ($value as $key => $arrayVal) {
$result[$key] = self::jsonDataToValue($arrayVal);
}
} else {
$result = self::fillObjFromValue($value, self::getObjFromClass($newClassName));
}
return $result;
case 'boolean':
case 'integer':
case 'double':
case 'string':
case "NULL":
return $value;
default:
return null;
}
}
/**
* Get object from class name, if class don't exists return StdClass.
* With PHP 5.4.0 the object is intialized without call the constructor.
*
* @param string $class class name
*
* @return object
*/
final protected static function getObjFromClass($class)
{
if (class_exists($class)) {
if (version_compare(PHP_VERSION, '5.4.0') >= 0) {
$classReflect = new ReflectionClass($class);
return $classReflect->newInstanceWithoutConstructor();
} else {
return new $class();
}
} else {
return new \StdClass();
}
}
/**
* Fill passed object from array values
*
* @param array $value value from json data
* @param object $obj object to fill with json data
*
* @return object
*/
final protected static function fillObjFromValue($value, $obj)
{
if ($obj instanceof \stdClass) {
foreach ($value as $arrayProp => $arrayValue) {
if ($arrayProp == self::CLASS_KEY_FOR_JSON_SERIALIZE) {
continue;
}
$obj->{$arrayProp} = self::jsonDataToValue($arrayValue);
}
} else {
$reflect = new ReflectionObject($obj);
foreach ($reflect->getProperties() as $prop) {
$prop->setAccessible(true);
$propName = $prop->getName();
if (!isset($value[$propName]) || $prop->isStatic()) {
continue;
}
$prop->setValue($obj, self::jsonDataToValue($value[$propName]));
}
if (method_exists($obj, '__wakeup')) {
$obj->__wakeup();
}
}
return $obj;
}
/**
* Return class name from array values
*
* @param array $array array data
*
* @return bool|string false if prop not found
*/
final protected static function getClassFromArray($array)
{
return (isset($array[self::CLASS_KEY_FOR_JSON_SERIALIZE]) ? $array[self::CLASS_KEY_FOR_JSON_SERIALIZE] : false);
}
}

View File

@@ -0,0 +1,83 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\Snap\JsonSerialize;
use Duplicator\Libs\Snap\SnapJson;
use Duplicator\Libs\Snap\SnapLog;
use Exception;
/**
* This class serializes and deserializes a variable in json keeping the class type and saving also private objects
*/
class JsonSerialize extends AbstractJsonSerializeObjData
{
/**
* Return json string
*
* @param mixed $value value to serialize
* @param integer $flags json_encode flags
* @param integer $depth json_encode depth
*
* @link https://www.php.net/manual/en/function.json-encode.php
*
* @return string|bool Returns a JSON encoded string on success or false on failure.
*/
public static function serialize($value, $flags = 0, $depth = 512)
{
return SnapJson::jsonEncode(self::valueToJsonData($value, $flags), $flags, $depth);
}
/**
* Unserialize from json
*
* @param string $json json string
* @param integer $depth json_decode depth
* @param integer $flags json_decode flags
*
* @link https://www.php.net/manual/en/function.json-decode.php
*
* @return mixed
*/
public static function unserialize($json, $depth = 512, $flags = 0)
{
// phpcs:ignore PHPCompatibility.FunctionUse.NewFunctionParameters.json_decode_optionsFound
$publicArray = (version_compare(PHP_VERSION, '5.4', '>=') ? json_decode($json, true, $depth, $flags) : json_decode($json, true, $depth)
);
return self::jsonDataToValue($publicArray);
}
/**
* Unserialize json on passed object
*
* @param string $json json string
* @param object|string $obj object to fill or class name
* @param integer $depth json_decode depth
* @param integer $flags json_decode flags
*
* @link https://www.php.net/manual/en/function.json-decode.php
*
* @return object
*/
public static function unserializeToObj($json, $obj, $depth = 512, $flags = 0)
{
if (is_object($obj)) {
} elseif (is_string($obj) && class_exists($obj)) {
$obj = self::getObjFromClass($obj);
} else {
throw new Exception('invalid obj param');
}
// phpcs:ignore PHPCompatibility.FunctionUse.NewFunctionParameters.json_decode_optionsFound
$value = (version_compare(PHP_VERSION, '5.4', '>=') ? json_decode($json, true, $depth, $flags) : json_decode($json, true, $depth)
);
if (!is_array($value)) {
throw new Exception('json value isn\'t an array VALUE: ' . SnapLog::v2str($value));
}
return self::fillObjFromValue($value, $obj);
}
}

View File

@@ -0,0 +1,13 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Libs\Snap;
class Snap32BitSizeLimitException extends \Exception
{
}

View File

@@ -0,0 +1,71 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Libs\Snap;
use Exception;
/**
* Snap code generator utils
*/
class SnapCode
{
/**
* Get class code from file
*
* @param string $file file path
* @param bool $wrapNamespace if true wrap name space with brackets
* @param bool $removeFirstPHPTag if true removes opening php tah
* @param bool $removeBalnkLines if true remove blank lines
* @param bool $removeComments if true remove comments
* @param bool $required if true and file can't be read then throw and exception else return empty string
*
* @return string
*/
public static function getSrcClassCode(
$file,
$wrapNamespace = true,
$removeFirstPHPTag = false,
$removeBalnkLines = true,
$removeComments = true,
$required = true
) {
if (!is_file($file) || !is_readable($file)) {
if ($required) {
throw new Exception('Code file "' . $file . '" don\'t exists');
}
return '';
}
if (($src = file_get_contents($file)) === false) {
if ($required) {
throw new Exception('Can\'t read code file "' . $file . '"');
}
return '';
}
if ($removeFirstPHPTag) {
$src = preg_replace('/^(<\?php)/', "", $src);
}
if ($wrapNamespace) {
$src = preg_replace('/(.*^\s*)(namespace.*?)(;)(.*)/sm', "$2 {\n$4}", $src);
}
if ($removeComments) {
$src = preg_replace('/^\s*\/\*.*?\*\//sm', '', $src);
$src = preg_replace('/^\s*\/\/.*$/m', '', $src);
}
if ($removeBalnkLines) {
$src = preg_replace('/\n\s*\n/s', "\n", $src);
}
return $src;
}
}

View File

@@ -0,0 +1,734 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Libs\Snap;
use Exception;
use mysqli;
use mysqli_result;
class SnapDB
{
const CONN_MYSQL = 'mysql';
const CONN_MYSQLI = 'mysqli';
const CACHE_PREFIX_PRIMARY_KEY_COLUMN = 'pkcol_';
const DB_ENGINE_MYSQL = 'MySQL';
const DB_ENGINE_MARIA = 'MariaDB';
const DB_ENGINE_PERCONA = 'Percona';
/** @var array<string, mixed> */
private static $cache = array();
/**
* Return array if primary key is composite key
*
* @param mysqli|resource $dbh database connection
* @param string $tableName table name
* @param null|callable $logCallback log callback
*
* @return false|string|string[] return unique index column ky or false if don't exists
*/
public static function getUniqueIndexColumn($dbh, $tableName, $logCallback = null)
{
$cacheKey = self::CACHE_PREFIX_PRIMARY_KEY_COLUMN . $tableName;
if (!isset(self::$cache[$cacheKey])) {
$query = 'SHOW COLUMNS FROM `' . self::realEscapeString($dbh, $tableName) . '` WHERE `Key` IN ("PRI","UNI")';
if (($result = self::query($dbh, $query)) === false) {
if (is_callable($logCallback)) {
call_user_func($logCallback, $dbh, $result, $query);
}
throw new \Exception('SHOW KEYS QUERY ERROR: ' . self::error($dbh));
}
if (is_callable($logCallback)) {
call_user_func($logCallback, $dbh, $result, $query);
}
if (self::numRows($result) == 0) {
self::$cache[$cacheKey] = false;
} else {
$primary = false;
$excludePrimary = false;
$unique = false;
while ($row = self::fetchAssoc($result)) {
switch ($row['Key']) {
case 'PRI':
if ($primary === false) {
$primary = $row['Field'];
} else {
if (is_scalar($primary)) {
$primary = array($primary);
}
$primary[] = $row['Field'];
}
if (preg_match('/^(?:var)?binary/i', $row['Type'])) {
// exclude binary or varbynary columns
$excludePrimary = true;
}
break;
case 'UNI':
if (!preg_match('/^(?:var)?binary/i', $row['Type'])) {
// exclude binary or varbynary columns
$unique = $row['Field'];
}
break;
default:
break;
}
}
if ($primary !== false && $excludePrimary === false) {
self::$cache[$cacheKey] = $primary;
} elseif ($unique !== false) {
self::$cache[$cacheKey] = $unique;
} else {
self::$cache[$cacheKey] = false;
}
}
self::freeResult($result);
}
return self::$cache[$cacheKey];
}
/**
* Escape the regex for mysql queries, the mysqli_real_escape must be applied anyway to the generated string
*
* @param string $regex Regex
*
* @return string Escaped regex
*/
public static function quoteRegex($regex)
{
// preg_quote takes a string and escapes special characters with a backslash.
// It is meant for PHP regexes, not MySQL regexes, and it does not escape &,
// which is needed for MySQL. So we only need to modify it like so:
// https://stackoverflow.com/questions/3782379/whats-the-best-way-to-escape-user-input-for-regular-expressions-in-mysql
return preg_replace('/&/', '\\&', preg_quote($regex, null /* no delimiter */));
}
/**
* Returns the offset from the current row
*
* @param mixed[] $row current database row
* @param int|string|string[] $indexColumns columns of the row that generated the index offset
* @param mixed $lastOffset last offset
*
* @return mixed
*/
public static function getOffsetFromRowAssoc($row, $indexColumns, $lastOffset)
{
if (is_array($indexColumns)) {
$result = array();
foreach ($indexColumns as $col) {
$result[$col] = isset($row[$col]) ? $row[$col] : 0;
}
return $result;
} elseif (strlen($indexColumns) > 0) {
return isset($row[$indexColumns]) ? $row[$indexColumns] : 0;
} else {
if (is_scalar($lastOffset)) {
return $lastOffset + 1;
} else {
return $lastOffset;
}
}
}
/**
* This function performs a select by structuring the primary key as offset if the table has a primary key.
* For optimization issues, no checks are performed on the input query and it is assumed that the select has at least a where value.
* If there are no conditions, you still have to perform an always true condition, for example
* SELECT * FROM `copy1_postmeta` WHERE 1
*
* @param mysqli|resource $dbh database connection
* @param string $query query string
* @param string $table table name
* @param int $offset row offset
* @param int $limit limit of query, 0 no limit
* @param mixed $lastRowOffset last offset to use on next function call
* @param null|callable $logCallback log callback
*
* @return mysqli_result
*/
public static function selectUsingPrimaryKeyAsOffset($dbh, $query, $table, $offset, $limit, &$lastRowOffset = null, $logCallback = null)
{
$where = '';
$orderby = '';
$offsetStr = '';
$limitStr = $limit > 0 ? ' LIMIT ' . $limit : '';
if (($primaryColumn = self::getUniqueIndexColumn($dbh, $table, $logCallback)) == false) {
$offsetStr = ' OFFSET ' . (is_scalar($offset) ? $offset : 0);
} else {
if (is_array($primaryColumn)) {
// COMPOSITE KEY
$orderByCols = array();
foreach ($primaryColumn as $colIndex => $col) {
$orderByCols[] = '`' . $col . '` ASC';
}
$orderby = ' ORDER BY ' . implode(',', $orderByCols);
} else {
$orderby = ' ORDER BY `' . $primaryColumn . '` ASC';
}
$where = self::getOffsetKeyCondition($dbh, $primaryColumn, $offset);
}
$query .= $where . $orderby . $limitStr . $offsetStr;
if (($result = self::query($dbh, $query)) === false) {
if (is_callable($logCallback)) {
call_user_func($logCallback, $dbh, $result, $query);
}
throw new \Exception('SELECT ERROR: ' . self::error($dbh) . ' QUERY: ' . $query);
}
if (is_callable($logCallback)) {
call_user_func($logCallback, $dbh, $result, $query);
}
if (self::dbConnTypeByResult($result) === self::CONN_MYSQLI) {
if ($primaryColumn == false) {
$lastRowOffset = $offset + $result->num_rows;
} else {
if ($result->num_rows == 0) {
$lastRowOffset = $offset;
} else {
$result->data_seek(($result->num_rows - 1));
$row = $result->fetch_assoc();
if (is_array($primaryColumn)) {
$lastRowOffset = array();
foreach ($primaryColumn as $col) {
$lastRowOffset[$col] = $row[$col];
}
} else {
$lastRowOffset = $row[$primaryColumn];
}
$result->data_seek(0);
}
}
} else {
if ($primaryColumn == false) {
$lastRowOffset = $offset + mysql_num_rows($result); // @phpstan-ignore-line
} else {
if (mysql_num_rows($result) == 0) { // @phpstan-ignore-line
$lastRowOffset = $offset;
} else {
mysql_data_seek($result, (mysql_num_rows($result) - 1)); // @phpstan-ignore-line
$row = mysql_fetch_assoc($result); // @phpstan-ignore-line
if (is_array($primaryColumn)) {
$lastRowOffset = array();
foreach ($primaryColumn as $col) {
$lastRowOffset[$col] = $row[$col];
}
} else {
$lastRowOffset = $row[$primaryColumn];
}
mysql_data_seek($result, 0); // @phpstan-ignore-line
}
}
}
return $result;
}
/**
* Depending on the structure type of the primary key returns the condition to position at the right offset
*
* @param mysqli|resource $dbh database connection
* @param string|string[] $primaryColumn primaricolumng index
* @param mixed $offset offset
*
* @return string
*/
protected static function getOffsetKeyCondition($dbh, $primaryColumn, $offset)
{
$condition = '';
if ($offset === 0) {
return '';
}
// COUPOUND KEY
if (is_array($primaryColumn)) {
$isFirstCond = true;
foreach ($primaryColumn as $colIndex => $col) {
if (is_array($offset) && isset($offset[$col])) {
if ($isFirstCond) {
$isFirstCond = false;
} else {
$condition .= ' OR ';
}
$condition .= ' (';
for ($prevColIndex = 0; $prevColIndex < $colIndex; $prevColIndex++) {
$condition .=
' `' . $primaryColumn[$prevColIndex] . '` = "' .
self::realEscapeString($dbh, $offset[$primaryColumn[$prevColIndex]]) . '" AND ';
}
$condition .= ' `' . $col . '` > "' . self::realEscapeString($dbh, $offset[$col]) . '")';
}
}
} else {
$condition = '`' . $primaryColumn . '` > "' . self::realEscapeString($dbh, (is_scalar($offset) ? $offset : 0)) . '"';
}
return (strlen($condition) ? ' AND (' . $condition . ')' : '');
}
/**
* get current database engine (mysql, maria, percona)
*
* @param mysqli|resource $dbh database connection
*
* @return string
*/
public static function getDBEngine($dbh)
{
if (($result = self::query($dbh, "SHOW VARIABLES LIKE 'version%'")) === false) {
// on query error assume is mysql.
return self::DB_ENGINE_MYSQL;
}
$rows = array();
while ($row = self::fetchRow($result)) {
$rows[] = $row;
}
self::freeResult($result);
$version = isset($rows[0][1]) ? $rows[0][1] : false;
$versionComment = isset($rows[1][1]) ? $rows[1][1] : false;
//Default is mysql
if ($version === false && $versionComment === false) {
return self::DB_ENGINE_MYSQL;
}
if (stripos($version, 'maria') !== false || stripos($versionComment, 'maria') !== false) {
return self::DB_ENGINE_MARIA;
}
if (stripos($version, 'percona') !== false || stripos($versionComment, 'percona') !== false) {
return self::DB_ENGINE_PERCONA;
}
return self::DB_ENGINE_MYSQL;
}
/**
* Escape string
*
* @param resource|mysqli $dbh database connection
* @param string $string string to escape
*
* @return string Returns an escaped string.
*/
public static function realEscapeString($dbh, $string)
{
if (self::dbConnType($dbh) === self::CONN_MYSQLI) {
return mysqli_real_escape_string($dbh, $string);
} else {
return mysql_real_escape_string($string, $dbh); // @phpstan-ignore-line
}
}
/**
*
* @param resource|mysqli $dbh database connection
* @param string $query query string
*
* @return mixed <p>Returns <b><code>FALSE</code></b> on failure. For successful <i>SELECT, SHOW, DESCRIBE</i> or
* <i>EXPLAIN</i> queries <b>mysqli_query()</b> will return a mysqli_result object.
* For other successful queries <b>mysqli_query()</b> will return <b><code>TRUE</code></b>.</p>
*/
public static function query($dbh, $query)
{
try {
if (self::dbConnType($dbh) === self::CONN_MYSQLI) {
return mysqli_query($dbh, $query);
} else {
return mysql_query($query, $dbh); // @phpstan-ignore-line
}
} catch (Exception $e) {
return false;
}
}
/**
*
* @param resource|mysqli_result $result query result
*
* @return int
*/
public static function numRows($result)
{
if (self::dbConnTypeByResult($result) === self::CONN_MYSQLI) {
return $result->num_rows;
} else {
return mysql_num_rows($result); // @phpstan-ignore-line
}
}
/**
*
* @param resource|mysqli_result $result query result
*
* @return string[]|null|false Returns an array of strings that corresponds to the fetched row. NULL if there are no more rows in result set
*/
public static function fetchRow($result)
{
if (self::dbConnTypeByResult($result) === self::CONN_MYSQLI) {
return mysqli_fetch_row($result);
} elseif (is_resource($result)) {
return mysql_fetch_row($result); // @phpstan-ignore-line
} else {
return false;
}
}
/**
*
* @param resource|mysqli_result $result query result
*
* @return string[]|null|false Returns an associative array of values representing the fetched row in the result set,
* where each key in the array represents the name of one of the result set's
* columns or null if there are no more rows in result set.
*/
public static function fetchAssoc($result)
{
if (self::dbConnTypeByResult($result) === self::CONN_MYSQLI) {
return mysqli_fetch_assoc($result);
} elseif (is_resource($result)) {
return mysql_fetch_assoc($result); // @phpstan-ignore-line
} else {
return false;
}
}
/**
*
* @param resource|mysqli_result $result query result
*
* @return boolean
*/
public static function freeResult($result)
{
if (self::dbConnTypeByResult($result) === self::CONN_MYSQLI) {
$result->free();
return true;
} elseif (is_resource($result)) {
return mysql_free_result($result); // @phpstan-ignore-line
} else {
$result = null;
return true;
}
}
/**
*
* @param resource|mysqli $dbh database connection
*
* @return string
*/
public static function error($dbh)
{
if (self::dbConnType($dbh) === self::CONN_MYSQLI) {
if ($dbh instanceof mysqli) {
return mysqli_error($dbh);
} else {
return 'Unable to retrieve the error message from MySQL';
}
} else {
if (is_resource($dbh)) {
return mysql_error($dbh); // @phpstan-ignore-line
} else {
return 'Unable to retrieve the error message from MySQL';
}
}
}
/**
*
* @param resource|mysqli $dbh database connection
*
* @return string // self::CONN_MYSQLI|self::CONN_MYSQL
*/
public static function dbConnType($dbh)
{
return (is_object($dbh) && get_class($dbh) == 'mysqli') ? self::CONN_MYSQLI : self::CONN_MYSQL;
}
/**
*
* @param resource|mysqli_result $result query resyult
*
* @return string Enum self::CONN_MYSQLI|self::CONN_MYSQL
*/
public static function dbConnTypeByResult($result)
{
return (is_object($result) && get_class($result) == 'mysqli_result') ? self::CONN_MYSQLI : self::CONN_MYSQL;
}
/**
* This function takes in input the values of a multiple inster with this format
* (v1, v2, v3 ...),(v1, v2, v3, ...),...
* and returns a two dimensional array where each item is a row containing the list of values
* [
* [v1, v2, v3 ...],
* [v1, v2, v3 ...],
* ...
* ]
* The return values are not processed but are taken exactly as they are in the dump file.
* So if they are escaped it remains unchanged
*
* @param string $query query values
*
* @return array<array<scalar>>
*/
public static function getValuesFromQueryInsert($query)
{
$result = array();
$isItemOpen = false;
$isStringOpen = false;
$char = '';
$pChar = '';
$currentItem = array();
$currentValue = '';
for ($i = 0; $i < strlen($query); $i++) {
$pChar = $char;
$char = $query[$i];
switch ($char) {
case '(':
if ($isItemOpen == false && !$isStringOpen) {
$isItemOpen = true;
continue 2;
}
break;
case ')':
if ($isItemOpen && !$isStringOpen) {
$isItemOpen = false;
$currentItem[] = trim($currentValue);
$currentValue = '';
$result[] = $currentItem;
$currentItem = array();
continue 2;
}
break;
case '\'':
case '"':
if ($isStringOpen === false && $pChar !== '\\') {
$isStringOpen = $char;
} elseif ($isStringOpen === $char && $pChar !== '\\') {
$isStringOpen = false;
}
break;
case ',':
if ($isItemOpen == false) {
continue 2;
} elseif ($isStringOpen === false) {
$currentItem[] = trim($currentValue);
$currentValue = '';
continue 2;
}
break;
default:
break;
}
if ($isItemOpen == false) {
continue;
}
$currentValue .= $char;
}
return $result;
}
/**
* This is the inverse of getValuesFromQueryInsert, from an array of values it returns the valody of an insert query
*
* @param mixed[] $values rows values
*
* @return string
*/
public static function getQueryInsertValuesFromArray(array $values)
{
return implode(
',',
array_map(
function ($rowVals) {
return '(' . implode(',', $rowVals) . ')';
},
$values
)
);
}
/**
* Returns the content of a value resulting from getValuesFromQueryInsert in string
* Then remove the outer quotes and escape
* "value\"test" become value"test
*
* @param string $value value
*
* @return string
*/
public static function parsedQueryValueToString($value)
{
$result = preg_replace('/^[\'"]?(.*?)[\'"]?$/s', '$1', $value);
return stripslashes($result);
}
/**
* Returns the content of a value resulting from getValuesFromQueryInsert in int
* Then remove the outer quotes and escape
* "100" become (int)100
*
* @param string $value value
*
* @return int
*/
public static function parsedQueryValueToInt($value)
{
return (int) preg_replace('/^[\'"]?(.*?)[\'"]?$/s', '$1', $value);
}
/**
* Return the list of mysqlrealconnect existing flags values from mask
*
* @see https://www.php.net/manual/en/mysqli.real-connect.php
*
* @param bool $returnStr if true return define string else values
* @param null|int[] $filter if not null only the values that exist and are contained in the array are returned
*
* @return int[]|string[]
*/
public static function getMysqlConnectFlagsList($returnStr = true, $filter = null)
{
static $flagsList = null;
if (is_null($flagsList)) {
$flagsList = array();
if (defined('MYSQLI_CLIENT_COMPRESS')) {
$flagsList[MYSQLI_CLIENT_COMPRESS] = 'MYSQLI_CLIENT_COMPRESS';
}
if (defined('MYSQLI_CLIENT_FOUND_ROWS')) {
$flagsList[MYSQLI_CLIENT_FOUND_ROWS] = 'MYSQLI_CLIENT_FOUND_ROWS';
}
if (defined('MYSQLI_CLIENT_IGNORE_SPACE')) {
$flagsList[MYSQLI_CLIENT_IGNORE_SPACE] = 'MYSQLI_CLIENT_IGNORE_SPACE';
}
if (defined('MYSQLI_CLIENT_INTERACTIVE')) {
$flagsList[MYSQLI_CLIENT_INTERACTIVE] = 'MYSQLI_CLIENT_INTERACTIVE';
}
if (defined('MYSQLI_CLIENT_SSL')) {
$flagsList[MYSQLI_CLIENT_SSL] = 'MYSQLI_CLIENT_SSL';
}
if (defined('MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT')) {
// phpcs:ignore PHPCompatibility.Constants.NewConstants.mysqli_client_ssl_dont_verify_server_certFound
$flagsList[MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT] = 'MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT';
}
}
if (is_null($filter)) {
$result = $flagsList;
} else {
$result = array();
foreach ($flagsList as $flagVal => $flag) {
if (!in_array($flagVal, $filter)) {
continue;
}
$result[$flagVal] = $flag;
}
}
if ($returnStr) {
return array_values($result);
} else {
return array_keys($result);
}
}
/**
* Return the list of mysqlrealconnect flags values from mask
*
* @see https://www.php.net/manual/en/mysqli.real-connect.php
*
* @param int $value mask value
*
* @return int[]
*/
public static function getMysqlConnectFlagsFromMaskVal($value)
{
/*
MYSQLI_CLIENT_COMPRESS 32
MYSQLI_CLIENT_FOUND_ROWS 2
MYSQLI_CLIENT_IGNORE_SPACE 256
MYSQLI_CLIENT_INTERACTIVE 1024
MYSQLI_CLIENT_SSL 2048
MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT 64
*/
$result = array();
foreach (self::getMysqlConnectFlagsList(false) as $flagVal) {
if (($value & $flagVal) > 0) {
$result[] = $flagVal;
}
}
return $result;
}
/**
* Returns a list of redundant case insensitive duplicate tables
*
* @param string $prefix The WP table prefix
* @param string[] $duplicates List of case insensitive duplicate table names
*
* @return string[]
*/
public static function getRedundantDuplicateTables($prefix, $duplicates)
{
//core tables are not redundant, check with priority
foreach (SnapWP::getSiteCoreTables() as $coreTable) {
if (($k = array_search($prefix . $coreTable, $duplicates)) !== false) {
unset($duplicates[$k]);
return array_values($duplicates);
}
}
foreach ($duplicates as $i => $tableName) {
if (stripos($tableName, $prefix) === 0) {
//table has prefix, the case sensitive match is not redundant
if (strpos($tableName, $prefix) === 0) {
unset($duplicates[$i]);
break;
}
//no case sensitive match is present, first table is not redundant
if ($i === (count($duplicates) - 1)) {
unset($duplicates[0]);
break;
}
} else {
//no prefix present, first table not redundant
unset($duplicates[$i]);
break;
}
}
return array_values($duplicates);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,302 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Libs\Snap;
// phpcs:disable
require_once(__DIR__ . '/JsonSerializable.php');
// phpcs:enable
class SnapJson
{
/**
* Encode a variable into JSON, with some sanity checks.
*
* @since 4.1.0
*
* @param mixed $data Variable (usually an array or object) to encode as JSON.
* @param int $options Optional. Options to be passed to json_encode(). Default 0.
* @param int $depth Optional. Maximum depth to walk through $data. Must be
* greater than 0. Default 512.
*
* @return string|false The JSON encoded string, or false if it cannot be encoded.
*/
public static function jsonEncode($data, $options = 0, $depth = 512)
{
if (function_exists('wp_json_encode')) {
return wp_json_encode($data, $options, $depth);
}
/*
* json_encode() has had extra params added over the years.
* $options was added in 5.3, and $depth in 5.5.
* We need to make sure we call it with the correct arguments.
*/
if (version_compare(PHP_VERSION, '5.5', '>=')) {
$args = array($data, $options, $depth);
} elseif (version_compare(PHP_VERSION, '5.3', '>=')) {
$args = array($data, $options);
} else {
$args = array($data);
}
$preparedData = self::jsonPrepareData($data);
// Prepare the data for JSON serialization.
$args[0] = $preparedData;
$json = @call_user_func_array('json_encode', $args);
// If json_encode() was successful, no need to do more sanity checking.
// ... unless we're in an old version of PHP, and json_encode() returned
// a string containing 'null'. Then we need to do more sanity checking.
if (false !== $json && ( version_compare(PHP_VERSION, '5.5', '>=') || false === strpos($json, 'null') )) {
return $json;
}
try {
$args[0] = self::jsonSanityCheck($preparedData, $depth);
} catch (\Exception $e) {
return false;
}
return call_user_func_array('json_encode', $args);
}
/**
* wp_json_encode with pretty print if define exists
*
* @param mixed $data Variable (usually an array or object) to encode as JSON.
* @param int $options Optional. Options to be passed to json_encode(). Default 0.
* @param int $depth Optional. Maximum depth to walk through $data. Must be
* greater than 0. Default 512.
*
* @return string|false The JSON encoded string, or false if it cannot be encoded.
*/
public static function jsonEncodePPrint($data, $options = 0, $depth = 512)
{
if (defined('JSON_PRETTY_PRINT')) {
// phpcs:ignore PHPCompatibility.Constants.NewConstants.json_pretty_printFound
return self::jsonEncode($data, JSON_PRETTY_PRINT | $options, $depth);
} else {
return self::jsonEncode($data, $options, $depth);
}
}
/**
* Prepares response data to be serialized to JSON.
*
* This supports the JsonSerializable interface for PHP 5.2-5.3 as well.
*
* @param mixed $data Native representation.
*
* @return bool|int|float|null|string|mixed[] Data ready for `json_encode()`.
*/
private static function jsonPrepareData($data)
{
if (
!defined('WP_JSON_SERIALIZE_COMPATIBLE') ||
WP_JSON_SERIALIZE_COMPATIBLE === false
) {
return $data;
}
switch (gettype($data)) {
case 'boolean':
case 'integer':
case 'double':
case 'string':
case 'NULL':
// These values can be passed through.
return $data;
case 'array':
// Arrays must be mapped in case they also return objects.
return array_map(array(__CLASS__, 'jsonPrepareData'), $data);
case 'object':
// If this is an incomplete object (__PHP_Incomplete_Class), bail.
if (!is_object($data)) {
return null;
}
if ($data instanceof \JsonSerializable) {
$data = $data->jsonSerialize();
} else {
$data = get_object_vars($data);
}
// Now, pass the array (or whatever was returned from jsonSerialize through).
return self::jsonPrepareData($data);
default:
return null;
}
}
/**
* Perform sanity checks on data that shall be encoded to JSON.
*
* @ignore
* @since 4.1.0
* @access private
*
* @see wp_json_encode()
*
* @param mixed $data Variable (usually an array or object) to encode as JSON.
* @param int $depth Maximum depth to walk through $data. Must be greater than 0.
*
* @return mixed The sanitized data that shall be encoded to JSON.
*/
private static function jsonSanityCheck($data, $depth)
{
if ($depth < 0) {
throw new \Exception('Reached depth limit');
}
if ($data instanceof \JsonSerializable) {
$data = $data->jsonSerialize();
}
if (is_array($data)) {
$output = array();
foreach ($data as $id => $el) {
// Don't forget to sanitize the ID!
if (is_string($id)) {
$clean_id = self::jsonConvertString($id);
} else {
$clean_id = $id;
}
// Check the element type, so that we're only recursing if we really have to.
if (is_array($el) || is_object($el)) {
$output[$clean_id] = self::jsonSanityCheck($el, $depth - 1);
} elseif (is_string($el)) {
$output[$clean_id] = self::jsonConvertString($el);
} else {
$output[$clean_id] = $el;
}
}
} elseif (is_object($data)) {
$output = new \stdClass();
foreach ($data as $id => $el) {
if (is_string($id)) {
$clean_id = self::jsonConvertString($id);
} else {
$clean_id = $id;
}
if (is_array($el) || is_object($el)) {
$output->$clean_id = self::jsonSanityCheck($el, $depth - 1);
} elseif (is_string($el)) {
$output->$clean_id = self::jsonConvertString($el);
} else {
$output->$clean_id = $el;
}
}
} elseif (is_string($data)) {
return self::jsonConvertString($data);
} else {
return $data;
}
return $output;
}
/**
* Return json string
*
* @param string $string data
*
* @return string
*/
private static function jsonConvertString($string)
{
static $use_mb = null;
if (is_null($use_mb)) {
$use_mb = function_exists('mb_convert_encoding');
}
if ($use_mb) {
$encoding = mb_detect_encoding($string, mb_detect_order(), true);
if ($encoding) {
return mb_convert_encoding($string, 'UTF-8', $encoding);
} else {
return mb_convert_encoding($string, 'UTF-8', 'UTF-8');
}
} else {
return self::checkInvalidUTF8($string, true);
}
}
/**
* Checks for invalid UTF8 in a string.
*
* @param string $string The text which is to be checked.
* @param bool $strip Optional. Whether to attempt to strip out invalid UTF8. Default is false.
*
* @return string The checked text.
*/
public static function checkInvalidUTF8($string, $strip = false)
{
$string = (string) $string;
if (0 === strlen($string)) {
return '';
}
// Check for support for utf8 in the installed PCRE library once and store the result in a static
static $utf8_pcre = null;
if (!isset($utf8_pcre)) {
$utf8_pcre = @preg_match('/^./u', 'a');
}
// We can't demand utf8 in the PCRE installation, so just return the string in those cases
if (!$utf8_pcre) {
return $string;
}
// preg_match fails when it encounters invalid UTF8 in $string
if (1 === @preg_match('/^./us', $string)) {
return $string;
}
// Attempt to strip the bad chars if requested (not recommended)
if ($strip && function_exists('iconv')) {
return iconv('utf-8', 'utf-8', $string);
}
return '';
}
/**
* todo remove esc_attr wp function
*
* @param mixed $val object to be encoded
*
* @return string escaped json string
*/
public static function jsonEncodeEscAttr($val)
{
return esc_attr(json_encode($val));
}
/**
* this function return a json encoded string without quotes at the beginning and the end
*
* @param string $string json string
*
* @return string
*/
public static function getJsonWithoutQuotes($string)
{
if (!is_string($string)) {
throw new \Exception('the function getJsonStringWithoutQuotes take only strings');
}
return substr(self::jsonEncode($string), 1, -1);
}
}

View File

@@ -0,0 +1,223 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Libs\Snap;
use Error;
use Exception;
class SnapLog
{
/** @var ?string */
public static $logFilepath = null;
/** @var ?resource */
public static $logHandle = null;
/**
* Init log file
*
* @param string $logFilepath lof file path
*
* @return void
*/
public static function init($logFilepath)
{
self::$logFilepath = $logFilepath;
}
/**
* write in PHP error log with DUP prefix
*
* @param string $message error message
* @param int $type error type
*
* @return bool true on success or false on failure.
*
* @link https://php.net/manual/en/function.error-log.php
*/
public static function phpErr($message, $type = 0)
{
if (function_exists('error_log')) {
return error_log('DUP:' . $message, $type);
} else {
return true;
}
}
/**
* Remove file log if exists
*
* @return void
*/
public static function clearLog()
{
if (file_exists(self::$logFilepath)) {
if (self::$logHandle !== null) {
fflush(self::$logHandle);
fclose(self::$logHandle);
self::$logHandle = null;
}
@unlink(self::$logFilepath);
}
}
/**
* Write in log passed object
*
* @param string $s log string
* @param mixed $o object to print
* @param boolean $flush if true flush log file
*
* @return void
*/
public static function logObject($s, $o, $flush = false)
{
self::log($s, $flush);
self::log(print_r($o, true), $flush);
}
/**
* Write in log file
*
* @param string $s string to write
* @param boolean $flush if true flush log file
* @param ?callable $callingFunctionOverride @deprecated 4.0.4 not used
*
* @return void
*/
public static function log($s, $flush = false, $callingFunctionOverride = null)
{
// echo "{$s}<br/>";
$lfp = self::$logFilepath;
// echo "logging $s to {$lfp}<br/>";
if (self::$logFilepath === null) {
throw new Exception('Logging not initialized');
}
if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
$timepart = $_SERVER['REQUEST_TIME_FLOAT'];
} else {
$timepart = $_SERVER['REQUEST_TIME'];
}
$thread_id = sprintf("%08x", abs(crc32($_SERVER['REMOTE_ADDR'] . $timepart . $_SERVER['REMOTE_PORT'])));
$s = $thread_id . ' ' . date('h:i:s') . ":$s";
if (self::$logHandle === null) {
self::$logHandle = fopen(self::$logFilepath, 'a');
}
fwrite(self::$logHandle, "$s\n");
if ($flush) {
fflush(self::$logHandle);
fclose(self::$logHandle);
self::$logHandle = fopen(self::$logFilepath, 'a');
}
}
/**
* Get formatted string fo value
*
* @param mixed $var value to convert to string
* @param bool $checkCallable if true check if var is callable and display it
*
* @return string
*/
public static function v2str($var, $checkCallable = false)
{
if ($checkCallable && is_callable($var)) {
return '(callable) ' . print_r($var, true);
}
switch (gettype($var)) {
case "boolean":
return $var ? 'true' : 'false';
case "integer":
case "double":
return (string) $var;
case "string":
return '"' . $var . '"';
case "array":
case "object":
return print_r($var, true);
case "resource":
case "resource (closed)":
case "NULL":
case "unknown type":
default:
return gettype($var);
}
}
/**
* Get backtrace of calling line
*
* @param string $message message
*
* @return string
*/
public static function getCurrentbacktrace($message = 'getCurrentLineTrace')
{
$callers = debug_backtrace();
array_shift($callers);
$file = $callers[0]['file'];
$line = $callers[0]['line'];
$result = 'BACKTRACE: ' . $message . "\n";
$result .= "\t[" . $file . ':' . $line . "]\n";
$result .= self::traceToString($callers, 1, true);
return $result;
}
/**
* Get trace string
*
* @param mixed[] $callers result of debug_backtrace
* @param int $fromLevel level to start
* @param bool $tab if true apply tab foreach line
*
* @return string
*/
public static function traceToString($callers, $fromLevel = 0, $tab = false)
{
$result = '';
for ($i = $fromLevel; $i < count($callers); $i++) {
$result .= ($tab ? "\t" : '');
$trace = $callers[$i];
if (!empty($trace['class'])) {
$result .= str_pad('TRACE[' . $i . '] CLASS___: ' . $trace['class'] . $trace['type'] . $trace['function'], 45, ' ');
} else {
$result .= str_pad('TRACE[' . $i . '] FUNCTION: ' . $trace['function'], 45, ' ');
}
if (isset($trace['file'])) {
$result .= ' FILE: ' . $trace['file'] . '[' . $trace['line'] . ']';
} else {
$result .= ' NO FILE';
}
$result .= "\n";
}
return $result;
}
/**
* Get exception message file line trace
*
* @param Exception|Error $e exception object
* @param bool $displayMessage if true diplay exception message
*
* @return string
*/
public static function getTextException($e, $displayMessage = true)
{
$result = ($displayMessage ? $e->getMessage() . "\n" : '');
return $result . "FILE:" . $e->getFile() . '[' . $e->getLIne() . "]\n" .
"TRACE:\n" . $e->getTraceAsString();
}
}

View File

@@ -0,0 +1,43 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Libs\Snap;
class SnapOS
{
const DEFAULT_WINDOWS_MAXPATH = 260;
const DEFAULT_LINUX_MAXPATH = 4096;
/**
* Return true if current SO is windows
*
* @return boolean
*/
public static function isWindows()
{
static $isWindows = null;
if (is_null($isWindows)) {
$isWindows = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN');
}
return $isWindows;
}
/**
* Return true if current SO is OSX
*
* @return boolean
*/
public static function isOSX()
{
static $isOSX = null;
if (is_null($isOSX)) {
$isOSX = (strtoupper(substr(PHP_OS, 0, 6)) === 'DARWIN');
}
return $isOSX;
}
}

View File

@@ -0,0 +1,361 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Libs\Snap;
/**
* Original installer files manager
*
* This class saves a file or folder in the original files folder and saves the original location persistent.
* By entry we mean a file or a folder but not the files contained within it.
* In this way it is possible, for example, to move an entire plugin to restore it later.
*/
class SnapOrigFileManager
{
const MODE_MOVE = 'move';
const MODE_COPY = 'copy';
const ORIG_FOLDER_PREFIX = 'original_files_';
const PERSISTANCE_FILE_NAME = 'entries_stored.json';
/** @var string */
protected $persistanceFile = null;
/** @var string */
protected $origFilesFolder = null;
/** @var array<string, array{baseName:string, source:string, stored: string, mode:string, isRelative: bool}> */
protected $origFolderEntries = array();
/** @var string */
protected $rootPath = null;
/**
* Class constructor
*
* @param string $root wordpress root path
* @param string $origFolderParentPath orig files folder path
* @param string $hash package hash
*/
public function __construct($root, $origFolderParentPath, $hash)
{
$this->rootPath = SnapIO::safePathUntrailingslashit($root, true);
$this->origFilesFolder = SnapIO::safePathTrailingslashit($origFolderParentPath, true) . self::ORIG_FOLDER_PREFIX . $hash;
$this->persistanceFile = $this->origFilesFolder . '/' . self::PERSISTANCE_FILE_NAME;
if (file_exists($this->persistanceFile)) {
$this->load();
}
}
/**
* Create a main folder if don't exist and load the entries
*
* @param boolean $reset if strue reset orig file folder
*
* @return void
*/
public function init($reset = false)
{
$this->createMainFolder($reset);
$this->load();
}
/**
* Create orig file folder
*
* @param boolean $reset if true delete current folder
*
* @return boolean return true if succeded
*
* @throws \Exception
*/
protected function createMainFolder($reset = false)
{
if ($reset) {
$this->deleteMainFolder();
}
if (!file_exists($this->origFilesFolder)) {
if (!SnapIO::mkdir($this->origFilesFolder, 'u+rwx')) {
throw new \Exception('Can\'t create the original files folder ' . SnapLog::v2str($this->origFilesFolder));
}
}
$htaccessFile = $this->origFilesFolder . '/.htaccess';
if (!file_exists($htaccessFile)) {
$content = <<<HTACCESS
Order Allow,Deny
Deny from All
HTACCESS;
@file_put_contents($htaccessFile, $content);
}
if (!file_exists($this->persistanceFile)) {
$this->save();
}
return true;
}
/**
* @return string Main folder path
* @throws \Exception
*/
public function getMainFolder()
{
if (!file_exists($this->origFilesFolder)) {
throw new \Exception('Can\'t get the original files folder ' . SnapLog::v2str($this->origFilesFolder));
}
return $this->origFilesFolder;
}
/**
* delete origianl files folder
*
* @return boolean
* @throws \Exception
*/
public function deleteMainFolder()
{
if (file_exists($this->origFilesFolder) && !SnapIO::rrmdir($this->origFilesFolder)) {
throw new \Exception('Can\'t delete the original files folder ' . SnapLog::v2str($this->origFilesFolder));
}
$this->origFolderEntries = array();
return true;
}
/**
* add a entry on original folder.
*
* @param string $identifier entry identifier
* @param string $path entry path. can be a file or a folder
* @param string $mode MODE_MOVE move the item in original folder
* MODE_COPY copy the item in original folder
* @param bool|string $rename if rename is a string the item is renamed in original folder.
*
* @return boolean true if succeded
*/
public function addEntry($identifier, $path, $mode = self::MODE_MOVE, $rename = false)
{
if (!file_exists($path)) {
return false;
}
$baseName = empty($rename) ? basename($path) : $rename;
if (($relativePath = SnapIO::getRelativePath($path, $this->rootPath)) === false) {
$isRelative = false;
} else {
$isRelative = true;
}
$parentFolder = $isRelative ? dirname($relativePath) : SnapIO::removeRootPath(dirname($path));
if (empty($parentFolder) || $parentFolder === '.') {
$parentFolder = '';
} else {
$parentFolder .= '/';
}
$targetFolder = $this->origFilesFolder . '/' . $parentFolder;
if (!file_exists($targetFolder)) {
SnapIO::mkdirP($targetFolder);
}
$dest = $targetFolder . $baseName;
switch ($mode) {
case self::MODE_MOVE:
// Don't use rename beacause new files must have the current script owner
if (!SnapIO::rcopy($path, $dest)) {
throw new \Exception('Can\'t copy the original file ' . SnapLog::v2str($path));
}
if (!SnapIO::rrmdir($path)) {
throw new \Exception('Can\'t remove the original file ' . SnapLog::v2str($path));
}
break;
case self::MODE_COPY:
if (!SnapIO::rcopy($path, $dest)) {
throw new \Exception('Can\'t copy the original file ' . SnapLog::v2str($path));
}
break;
default:
throw new \Exception('invalid mode addEntry');
}
$this->origFolderEntries[$identifier] = array(
'baseName' => $baseName,
'source' => $isRelative ? $relativePath : $path,
'stored' => $parentFolder . $baseName,
'mode' => $mode,
'isRelative' => $isRelative
);
$this->save();
return true;
}
/**
* Get entry info from identifier
*
* @param string $identifier orig file identifier
*
* @return false|array{baseName:string, source:string, stored: string, mode:string, isRelative: bool} false if entry don't exists
*/
public function getEntry($identifier)
{
if (isset($this->origFolderEntries[$identifier])) {
return $this->origFolderEntries[$identifier];
} else {
return false;
}
}
/**
* Get entry stored path in original folder
*
* @param string $identifier orig file identifier
*
* @return boolean|string false if entry don't exists
*/
public function getEntryStoredPath($identifier)
{
if (isset($this->origFolderEntries[$identifier])) {
return $this->origFilesFolder . '/' . $this->origFolderEntries[$identifier]['stored'];
} else {
return false;
}
}
/**
* Return true if identifier org file is relative path
*
* @param string $identifier orig file identifier
*
* @return boolean
*/
public function isRelative($identifier)
{
if (isset($this->origFolderEntries[$identifier])) {
return $this->origFolderEntries[$identifier]['isRelative'];
} else {
return false;
}
}
/**
* Get entry target restore path
*
* @param string $identifier orig file identifier
* @param null|string $defaultIfIsAbsolute if isn't null return the value if path is absolute
*
* @return false|string false if entry don't exists
*/
public function getEntryTargetPath($identifier, $defaultIfIsAbsolute = null)
{
if (isset($this->origFolderEntries[$identifier])) {
if ($this->origFolderEntries[$identifier]['isRelative']) {
return $this->rootPath . '/' . $this->origFolderEntries[$identifier]['source'];
} else {
if (is_null($defaultIfIsAbsolute)) {
return $this->origFolderEntries[$identifier]['source'];
} else {
return $defaultIfIsAbsolute;
}
}
} else {
return false;
}
}
/**
* this function restore current entry in original position.
* If mode is copy it simply delete the entry else move the entry in original position
*
* @param string $identifier identified of current entrye
* @param boolean $save update saved entries
* @param null|string $defaultIfIsAbsolute if isn't null return the value if path is absolute
*
* @return boolean true if succeded
*/
public function restoreEntry($identifier, $save = true, $defaultIfIsAbsolute = null)
{
if (!isset($this->origFolderEntries[$identifier])) {
return false;
}
$stored = $this->getEntryStoredPath($identifier);
if (($original = $this->getEntryTargetPath($identifier, $defaultIfIsAbsolute)) === false) {
return false;
}
switch ($this->origFolderEntries[$identifier]['mode']) {
case self::MODE_MOVE:
if (!SnapIO::rename($stored, $original)) {
throw new \Exception('Can\'t move the original file ' . SnapLog::v2str($stored));
}
break;
case self::MODE_COPY:
if (!SnapIO::rrmdir($stored)) {
throw new \Exception('Can\'t delete entry ' . SnapLog::v2str($stored));
}
break;
default:
throw new \Exception('invalid mode addEntry');
}
unset($this->origFolderEntries[$identifier]);
if ($save) {
$this->save();
}
return true;
}
/**
* Put all entries on original position and empty original folder
*
* @param string[] $exclude identifiers list t exclude
*
* @return boolean
*/
public function restoreAll($exclude = array())
{
foreach (array_keys($this->origFolderEntries) as $ident) {
if (in_array($ident, $exclude)) {
continue;
}
$this->restoreEntry($ident, false);
}
$this->save();
return true;
}
/**
* Save notices from json file
*
* @return bool
*/
public function save()
{
if (!file_put_contents($this->persistanceFile, SnapJson::jsonEncodePPrint($this->origFolderEntries))) {
throw new \Exception('Can\'t write persistence file');
}
return true;
}
/**
* Load notice from json file
*
* @return bool
*/
private function load()
{
if (file_exists($this->persistanceFile)) {
$json = file_get_contents($this->persistanceFile);
$this->origFolderEntries = json_decode($json, true);
} else {
$this->origFolderEntries = array();
}
return true;
}
}

View File

@@ -0,0 +1,186 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Libs\Snap;
class SnapString
{
/**
* Return true or false in string
*
* @param mixed $b input value
*
* @return string
*/
public static function boolToString($b)
{
return ($b ? 'true' : 'false');
}
/**
* Truncate string and add ellipsis
*
* @param string $s string to truncate
* @param int $maxWidth max length
*
* @return string
*/
public static function truncateString($s, $maxWidth)
{
if (strlen($s) > $maxWidth) {
$s = substr($s, 0, $maxWidth - 3) . '...';
}
return $s;
}
/**
* Returns true if the $haystack string starts with the $needle
*
* @param string $haystack The full string to search in
* @param string $needle The string to for
*
* @return bool Returns true if the $haystack string starts with the $needle
*/
public static function startsWith($haystack, $needle)
{
return (strpos($haystack, $needle) === 0);
}
/**
* Returns true if the $haystack string end with the $needle
*
* @param string $haystack The full string to search in
* @param string $needle The string to for
*
* @return bool Returns true if the $haystack string starts with the $needle
*/
public static function endsWith($haystack, $needle)
{
$length = strlen($needle);
if ($length == 0) {
return true;
}
return (substr($haystack, -$length) === $needle);
}
/**
* Returns true if the $needle is found in the $haystack
*
* @param string $haystack The full string to search in
* @param string $needle The string to for
*
* @return bool
*/
public static function contains($haystack, $needle)
{
$pos = strpos($haystack, $needle);
return ($pos !== false);
}
/**
* Implode array key values to a string
*
* @param string $glue separator
* @param mixed[] $pieces array fo implode
* @param string $format format
*
* @return string
*/
public static function implodeKeyVals($glue, $pieces, $format = '%s="%s"')
{
$strList = array();
foreach ($pieces as $key => $value) {
if (is_scalar($value)) {
$strList[] = sprintf($format, $key, $value);
} else {
$strList[] = sprintf($format, $key, print_r($value, true));
}
}
return implode($glue, $strList);
}
/**
* Replace last occurrence
*
* @param string $search The value being searched for
* @param string $replace The replacement value that replaces found search values
* @param string $str The string or array being searched and replaced on, otherwise known as the haystack
* @param boolean $caseSensitive Whether the replacement should be case sensitive or not
*
* @return string
*/
public static function strLastReplace($search, $replace, $str, $caseSensitive = true)
{
$pos = $caseSensitive ? strrpos($str, $search) : strripos($str, $search);
if (false !== $pos) {
$str = substr_replace($str, $replace, $pos, strlen($search));
}
return $str;
}
/**
* Check if passed string have html tags
*
* @param string $string input string
*
* @return boolean
*/
public static function isHTML($string)
{
return ($string != strip_tags($string));
}
/**
* Safe way to get number of characters
*
* @param ?string $string input string
*
* @return int
*/
public static function stringLength($string)
{
if (!isset($string) || $string == "") { // null == "" is also true
return 0;
}
return strlen($string);
}
/**
* Returns case insensitive duplicates
*
* @param string[] $strings The array of strings to check for duplicates
*
* @return array<string[]>
*/
public static function getCaseInsesitiveDuplicates($strings)
{
$duplicates = array();
for ($i = 0; $i < count($strings) - 1; $i++) {
$key = strtolower($strings[$i]);
//already found all instances so don't check again
if (isset($duplicates[$key])) {
continue;
}
for ($j = $i + 1; $j < count($strings); $j++) {
if ($strings[$i] !== $strings[$j] && $key === strtolower($strings[$j])) {
$duplicates[$key][] = $strings[$j];
}
}
//duplicates were found, add the comparing string to list
if (isset($duplicates[$key])) {
$duplicates[$key][] = $strings[$i];
}
}
return $duplicates;
}
}

View File

@@ -0,0 +1,237 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Libs\Snap;
class SnapURL
{
/** @var array<string, scalar> */
protected static $DEF_ARRAY_PARSE_URL = array(
'scheme' => false,
'host' => false,
'port' => false,
'user' => false,
'pass' => false,
'path' => '',
'query' => false,
'fragment' => false
);
/**
* Append a new query value to the end of a URL
*
* @param string $url The URL to append the new value to
* @param string $key The new key name
* @param ?scalar $value The new key name value
*
* @return string Returns the new URL with with the query string name and value
*/
public static function appendQueryValue($url, $key, $value)
{
$separator = (parse_url($url, PHP_URL_QUERY) == null) ? '?' : '&';
$modified_url = $url . "$separator$key=" . $value;
return $modified_url;
}
/**
* Add www. in url if don't have
*
* @param string $url input URL
*
* @return string
*/
public static function wwwAdd($url)
{
return preg_replace('/^((?:\w+\:)?\/\/)?(?!www\.)(.+)/', '$1www.$2', $url);
}
/**
* Remove www. in url if don't have
*
* @param string $url input URL
*
* @return string
*/
public static function wwwRemove($url)
{
return preg_replace('/^((?:\w+\:)?\/\/)?www\.(.+)/', '$1$2', $url);
}
/**
* Fetches current URL via PHP
*
* @param bool $queryString If true the query string will also be returned.
* @param boolean $requestUri If true check REQUEST_URI else SCRIPT_NAME
* @param int $getParentDirLevel If 0 get current script name or parent folder, if 1 parent folder if 2 parent of parent folder ...
*
* @return string The current page url
*/
public static function getCurrentUrl($queryString = true, $requestUri = false, $getParentDirLevel = 0)
{
// *** HOST
if (isset($_SERVER['HTTP_X_ORIGINAL_HOST'])) {
$host = $_SERVER['HTTP_X_ORIGINAL_HOST'];
} else {
$host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : $_SERVER['SERVER_NAME']; //WAS SERVER_NAME and caused problems on some boxes
}
// *** PROTOCOL
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$_SERVER ['HTTPS'] = 'on';
}
if (isset($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'https') {
$_SERVER ['HTTPS'] = 'on';
}
if (isset($_SERVER['HTTP_CF_VISITOR'])) {
$visitor = json_decode($_SERVER['HTTP_CF_VISITOR']);
if (is_object($visitor) && property_exists($visitor, 'scheme') && $visitor->scheme == 'https') {
$_SERVER ['HTTPS'] = 'on';
}
}
$protocol = 'http' . ((isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) === 'on') ? 's' : '');
if ($requestUri) {
$serverUrlSelf = preg_replace('/\?.*$/', '', $_SERVER['REQUEST_URI']);
} else {
// *** SCRIPT NAME
$serverUrlSelf = $_SERVER['SCRIPT_NAME'];
for ($i = 0; $i < $getParentDirLevel; $i++) {
$serverUrlSelf = preg_match('/^[\\\\\/]?$/', dirname($serverUrlSelf)) ? '' : dirname($serverUrlSelf);
}
}
// *** QUERY STRING
$query = ($queryString && isset($_SERVER['QUERY_STRING']) && strlen($_SERVER['QUERY_STRING']) > 0 ) ? '?' . $_SERVER['QUERY_STRING'] : '';
return $protocol . '://' . $host . $serverUrlSelf . $query;
}
/**
* Get current query string data array
*
* @return string[]
*/
public static function getCurrentQueryURLdata()
{
$result = array();
if (!isset($_SERVER['QUERY_STRING']) || strlen($_SERVER['QUERY_STRING']) == 0) {
return $result;
}
parse_str($_SERVER['QUERY_STRING'], $result);
return $result;
}
/**
* this function is a native PHP parse_url wrapper
* this function returns an associative array with all the keys present and the values = false if they do not exist.
*
* @param string $url <p>The URL to parse. Invalid characters are replaced by <i>_</i>.</p>
* @param int $component if != 1 return specific URL component
*
* @return mixed[]|string|int|null|false <p>On seriously malformed URLs, <b>parse_url()</b> may return <b><code>FALSE</code></b>.</p>
* <p>If the <code>component</code> parameter is omitted, an associative <code>array</code> is returned.
* At least one element will be present within the array. Potential keys within this array are:</p>
* <ul>
* <li> scheme - e.g. http </li>
* <li> host </li>
* <li> port </li>
* <li> user </li>
* <li> pass </li>
* <li> path </li>
* <li> query - after the question mark <i>&#63;</i> </li>
* <li> fragment - after the hashmark <i>#</i> </li>
* </ul>
* <p>If the <code>component</code> parameter is specified,
* <b>parse_url()</b> returns a <code>string</code> (or an <code>integer</code>,
* in the case of <b><code>PHP_URL_PORT</code></b>) instead of an <code>array</code>.
* If the requested component doesn't exist within the given URL, <b><code>NULL</code></b> will be returned.</p>
*/
public static function parseUrl($url, $component = -1)
{
if (preg_match('/^([a-zA-Z0-9]+\:)?\/\//', $url) !== 1) {
// fix invalid URL for only host string ex. 'myhost.com'
$url = '//' . $url;
}
$result = parse_url($url, $component);
if (is_array($result)) {
$result = array_merge(self::$DEF_ARRAY_PARSE_URL, $result);
}
return $result;
}
/**
* Remove scheme from URL
*
* @param string $url source url
* @param bool $removeWww if true remove www
*
* @return string
*/
public static function removeScheme($url, $removeWww = false)
{
$parts = self::parseUrl($url);
unset($parts['scheme']);
$result = self::buildUrl($parts);
if ($removeWww) {
$result = self::wwwRemove($result);
}
return ltrim($result, '/');
}
/**
* this function build a url from array result of parse url.
* if work with both parse_url native function result and snap parseUrl result
*
* @param array<string, mixed> $parts url parts from parseUrl
*
* @return bool|string return false if param isn't array
*/
public static function buildUrl($parts)
{
if (!is_array($parts)) {
return false;
}
$result = '';
$result .= (isset($parts['scheme']) && $parts['scheme'] !== false) ? $parts['scheme'] . ':' : '';
$result .= (
(isset($parts['user']) && $parts['user'] !== false) ||
(isset($parts['host']) && $parts['host'] !== false)) ? '//' : '';
$result .= (isset($parts['user']) && $parts['user'] !== false) ? $parts['user'] : '';
$result .= (isset($parts['pass']) && $parts['pass'] !== false) ? ':' . $parts['pass'] : '';
$result .= (isset($parts['user']) && $parts['user'] !== false) ? '@' : '';
$result .= (isset($parts['host']) && $parts['host'] !== false) ? $parts['host'] : '';
$result .= (isset($parts['port']) && $parts['port'] !== false) ? ':' . $parts['port'] : '';
$result .= (isset($parts['path']) && $parts['path'] !== false) ? $parts['path'] : '';
$result .= (isset($parts['query']) && $parts['query'] !== false) ? '?' . $parts['query'] : '';
$result .= (isset($parts['fragment']) && $parts['fragment'] !== false) ? '#' . $parts['fragment'] : '';
return $result;
}
/**
* Encode alla chars
*
* @param string $url input URL
*
* @return string
*/
public static function urlEncodeAll($url)
{
$hex = unpack('H*', urldecode($url));
return preg_replace('~..~', '%$0', strtoupper($hex[1]));
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
<?php
//silent

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,496 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\WpConfig;
use Exception;
/**
* Transforms a wp-config.php file.
* Fork of wp-cli/trnasformer
*/
class WPConfigTransformer
{
const REPLACE_TEMP_STIRNG = '_1_2_RePlAcE_3_4_TeMp_5_6_StRiNg_7_8_';
/**
* Path to the wp-config.php file.
*
* @var string
*/
protected $wp_config_path;
/**
* Original source of the wp-config.php file.
*
* @var string
*/
protected $wp_config_src;
/**
* Array of parsed configs.
*
* @var array
*/
protected $wp_configs = array();
/**
* Instantiates the class with a valid wp-config.php.
*
* @throws Exception If the wp-config.php file is missing.
* @throws Exception If the wp-config.php file is not writable.
*
* @param string $wp_config_path Path to a wp-config.php file.
*/
public function __construct($wp_config_path)
{
if (! file_exists($wp_config_path)) {
throw new Exception('wp-config.php file does not exist. Path:' . $wp_config_path);
}
// Duplicator Extra
/*
if ( ! is_writable( $wp_config_path ) ) {
throw new Exception( 'wp-config.php file is not writable.' );
}
*/
$this->wp_config_path = $wp_config_path;
}
/**
* Checks if a config exists in the wp-config.php file.
*
* @throws Exception If the wp-config.php file is empty.
* @throws Exception If the requested config type is invalid.
*
* @param string $type Config type (constant or variable).
* @param string $name Config name.
*
* @return bool
*/
public function exists($type, $name)
{
$wp_config_src = file_get_contents($this->wp_config_path);
if (! trim($wp_config_src)) {
throw new Exception('wp-config.php file is empty.');
}
// SnapCreek custom change
// Normalize the newline to prevent an issue coming from OSX
$wp_config_src = str_replace(array("\r\n", "\r"), "\n", $wp_config_src);
$this->wp_config_src = $wp_config_src;
$this->wp_configs = $this->parseWpConfig($this->wp_config_src);
if (! isset($this->wp_configs[ $type ])) {
throw new Exception("Config type '{$type}' does not exist.");
}
return isset($this->wp_configs[ $type ][ $name ]);
}
/**
* Get the value of a config in the wp-config.php file.
*
* @throws Exception If the wp-config.php file is empty.
* @throws Exception If the requested config type is invalid.
*
* @param string $type Config type (constant or variable).
* @param string $name Config name.
* @param bool $get_real_value if true return real value
*
* @return array
*/
public function getValue($type, $name, $get_real_value = true)
{
$wp_config_src = file_get_contents($this->wp_config_path);
if (! trim($wp_config_src)) {
throw new Exception('wp-config.php file is empty.');
}
// SnapCreek custom change
// Normalize the newline to prevent an issue coming from OSX
$wp_config_src = str_replace(array("\r\n", "\r"), "\n", $wp_config_src);
$this->wp_config_src = $wp_config_src;
$this->wp_configs = $this->parseWpConfig($this->wp_config_src);
if (! isset($this->wp_configs[ $type ])) {
throw new Exception("Config type '{$type}' does not exist.");
}
// Duplicator Extra
$val = $this->wp_configs[ $type ][ $name ]['value'];
if ($get_real_value) {
return self::getRealValFromVal($val);
} else {
return $val;
}
return $val;
}
/**
* Get typed val from string val
*
* @param string $val string value
*
* @return mixed
*/
public static function getRealValFromVal($val)
{
if ($val[0] === '\'') {
// string with '
$result = substr($val, 1, strlen($val) - 2);
return str_replace(array('\\\'', '\\\\'), array('\'', '\\'), $result);
} elseif ($val[0] === '"') {
// string with "
return json_decode(str_replace('\\$', '$', $val));
} elseif (strcasecmp($val, 'true') === 0) {
return true;
} elseif (strcasecmp($val, 'false') === 0) {
return false;
} elseif (strcasecmp($val, 'null') === 0) {
return null;
} elseif (preg_match('/^[-+]?[0-9]+$/', $val)) {
return (int) $val;
} elseif (preg_match('/^[-+]?[0-9]+\.[0-9]+$/', $val)) {
return (float) $val;
} else {
return $val;
}
}
/**
* Adds a config to the wp-config.php file.
*
* @throws Exception If the config value provided is not a string.
* @throws Exception If the config placement anchor could not be located.
*
* @param string $type Config type (constant or variable).
* @param string $name Config name.
* @param string $value Config value.
* @param array $options (optional) Array of special behavior options.
*
* @return bool
*/
public function add($type, $name, $value, array $options = array())
{
if (! is_string($value)) {
throw new Exception('Config value must be a string.');
}
if ($this->exists($type, $name)) {
return false;
}
$defaults = array(
'raw' => false, // Display value in raw format without quotes.
'anchor' => "/* That's all, stop editing!", // Config placement anchor string.
'separator' => PHP_EOL, // Separator between config definition and anchor string.
'placement' => 'before', // Config placement direction (insert before or after).
);
list( $raw, $anchor, $separator, $placement ) = array_values(array_merge($defaults, $options));
$raw = (bool) $raw;
$anchor = (string) $anchor;
$separator = (string) $separator;
$placement = (string) $placement;
// Custom code by the SnapCreek Team
if (false === strpos($this->wp_config_src, $anchor)) {
$other_anchor_points = array(
'/** Absolute path to the WordPress directory',
// ABSPATH defined check with single quote
"if ( !defined('ABSPATH') )",
"if ( ! defined( 'ABSPATH' ) )",
"if (!defined('ABSPATH') )",
"if(!defined('ABSPATH') )",
"if(!defined('ABSPATH'))",
"if ( ! defined( 'ABSPATH' ))",
"if ( ! defined( 'ABSPATH') )",
"if ( ! defined('ABSPATH' ) )",
"if (! defined( 'ABSPATH' ))",
"if (! defined( 'ABSPATH') )",
"if (! defined('ABSPATH' ) )",
"if ( !defined( 'ABSPATH' ))",
"if ( !defined( 'ABSPATH') )",
"if ( !defined('ABSPATH' ) )",
"if( !defined( 'ABSPATH' ))",
"if( !defined( 'ABSPATH') )",
"if( !defined('ABSPATH' ) )",
// ABSPATH defined check with double quote
'if ( !defined("ABSPATH") )',
'if ( ! defined( "ABSPATH" ) )',
'if (!defined("ABSPATH") )',
'if(!defined("ABSPATH") )',
'if(!defined("ABSPATH"))',
'if ( ! defined( "ABSPATH" ))',
'if ( ! defined( "ABSPATH") )',
'if ( ! defined("ABSPATH" ) )',
'if (! defined( "ABSPATH" ))',
'if (! defined( "ABSPATH") )',
'if (! defined("ABSPATH" ) )',
'if ( !defined( "ABSPATH" ))',
'if ( !defined( "ABSPATH") )',
'if ( !defined("ABSPATH" ) )',
'if( !defined( "ABSPATH" ))',
'if( !defined( "ABSPATH") )',
'if( !defined("ABSPATH" ) )',
'/** Sets up WordPress vars and included files',
'require_once(ABSPATH',
'require_once ABSPATH',
'require_once( ABSPATH',
'require_once',
"define( 'DB_NAME'",
'define( "DB_NAME"',
"define('DB_NAME'",
'define("DB_NAME"',
'require',
'include_once',
);
foreach ($other_anchor_points as $anchor_point) {
$anchor_point = (string) $anchor_point;
if (false !== strpos($this->wp_config_src, $anchor_point)) {
$anchor = $anchor_point;
break;
}
}
}
if (false === strpos($this->wp_config_src, $anchor)) {
throw new Exception('Unable to locate placement anchor.');
}
$new_src = $this->normalize($type, $name, $this->formatValue($value, $raw));
$new_src = ( 'after' === $placement ) ? $anchor . $separator . $new_src : $new_src . $separator . $anchor;
$contents = str_replace($anchor, $new_src, $this->wp_config_src);
return $this->save($contents);
}
/**
* Updates an existing config in the wp-config.php file.
*
* @throws Exception If the config value provided is not a string.
*
* @param string $type Config type (constant or variable).
* @param string $name Config name.
* @param string $value Config value.
* @param array $options (optional) Array of special behavior options.
*
* @return bool
*/
public function update($type, $name, $value, array $options = array())
{
if (! is_string($value)) {
throw new Exception('Config value must be a string.');
}
$defaults = array(
'add' => true, // Add the config if missing.
'raw' => false, // Display value in raw format without quotes.
'normalize' => false, // Normalize config output using WP Coding Standards.
);
list( $add, $raw, $normalize ) = array_values(array_merge($defaults, $options));
$add = (bool) $add;
$raw = (bool) $raw;
$normalize = (bool) $normalize;
if (! $this->exists($type, $name)) {
return ( $add ) ? $this->add($type, $name, $value, $options) : false;
}
$old_src = $this->wp_configs[ $type ][ $name ]['src'];
$old_value = $this->wp_configs[ $type ][ $name ]['value'];
$new_value = $this->formatValue($value, $raw);
if ($normalize) {
$new_src = $this->normalize($type, $name, $new_value);
} else {
$new_parts = $this->wp_configs[ $type ][ $name ]['parts'];
$new_parts[1] = str_replace($old_value, $new_value, $new_parts[1]); // Only edit the value part.
$new_src = implode('', $new_parts);
}
$contents = preg_replace(
sprintf('/(?<=^|;|<\?php\s|<\?\s)(\s*?)%s/m', preg_quote(trim($old_src), '/')),
'$1' . self::REPLACE_TEMP_STIRNG,
$this->wp_config_src
);
$contents = str_replace(self::REPLACE_TEMP_STIRNG, trim($new_src), $contents);
return $this->save($contents);
}
/**
* Removes a config from the wp-config.php file.
*
* @param string $type Config type (constant or variable).
* @param string $name Config name.
*
* @return bool
*/
public function remove($type, $name)
{
if (! $this->exists($type, $name)) {
return false;
}
$pattern = sprintf('/(?<=^|;|<\?php\s|<\?\s)%s\s*(\S|$)/m', preg_quote($this->wp_configs[ $type ][ $name ]['src'], '/'));
$contents = preg_replace($pattern, '$1', $this->wp_config_src);
return $this->save($contents);
}
/**
* Applies formatting to a config value.
*
* @throws Exception When a raw value is requested for an empty string.
*
* @param string $value Config value.
* @param bool $raw Display value in raw format without quotes.
*
* @return mixed
*/
protected function formatValue($value, $raw)
{
if ($raw && '' === trim($value)) {
throw new Exception('Raw value for empty string not supported.');
}
return ( $raw ) ? $value : var_export($value, true);
}
/**
* Normalizes the source output for a name/value pair.
*
* @throws Exception If the requested config type does not support normalization.
*
* @param string $type Config type (constant or variable).
* @param string $name Config name.
* @param mixed $value Config value.
*
* @return string
*/
protected function normalize($type, $name, $value)
{
if ('constant' === $type) {
$placeholder = "define( '%s', %s );";
} elseif ('variable' === $type) {
$placeholder = '$%s = %s;';
} else {
throw new Exception("Unable to normalize config type '{$type}'.");
}
return sprintf($placeholder, $name, $value);
}
/**
* Parses the source of a wp-config.php file.
*
* @param string $src Config file source.
*
* @return array
*/
protected function parseWpConfig($src)
{
$configs = array();
$configs['constant'] = array();
$configs['variable'] = array();
if (function_exists('token_get_all')) {
// Strip comments.
foreach (token_get_all($src) as $token) {
if (in_array($token[0], array( T_COMMENT, T_DOC_COMMENT ), true)) {
$src = str_replace($token[1], '', $src);
}
}
}
preg_match_all(
'/(?<=^|;|<\?php\s|<\?\s)' .
'(\h*define\s*\(\s*[\'"](\w*?)[\'"]\s*)(,\s*(\'\'|""|\'.*?[^\\\\]\'|".*?[^\\\\]"|.*?)\s*)' .
'((?:,\s*(?:true|false)\s*)?\)\s*;)/ims',
$src,
$constants
);
preg_match_all('/(?<=^|;|<\?php\s|<\?\s)(\h*\$(\w+)\s*=)(\s*(\'\'|""|\'.*?[^\\\\]\'|".*?[^\\\\]"|.*?)\s*;)/ims', $src, $variables);
if (
!empty($constants[0]) &&
!empty($constants[1]) &&
!empty($constants[2]) &&
!empty($constants[3]) &&
!empty($constants[4]) &&
!empty($constants[5])
) {
foreach ($constants[2] as $index => $name) {
$configs['constant'][ $name ] = array(
'src' => $constants[0][ $index ],
'value' => $constants[4][ $index ],
'parts' => array(
$constants[1][ $index ],
$constants[3][ $index ],
$constants[5][ $index ],
),
);
}
}
if (! empty($variables[0]) && ! empty($variables[1]) && ! empty($variables[2]) && ! empty($variables[3]) && ! empty($variables[4])) {
// Remove duplicate(s), last definition wins.
$variables[2] = array_reverse(array_unique(array_reverse($variables[2], true)), true);
foreach ($variables[2] as $index => $name) {
$configs['variable'][ $name ] = array(
'src' => $variables[0][ $index ],
'value' => $variables[4][ $index ],
'parts' => array(
$variables[1][ $index ],
$variables[3][ $index ],
),
);
}
}
return $configs;
}
/**
* Saves new contents to the wp-config.php file.
*
* @throws Exception If the config file content provided is empty.
* @throws Exception If there is a failure when saving the wp-config.php file.
*
* @param string $contents New config contents.
*
* @return bool
*/
protected function save($contents)
{
if (!trim($contents)) {
throw new Exception('Cannot save the wp-config.php file with empty contents.');
}
if ($contents === $this->wp_config_src) {
return false;
}
$result = file_put_contents($this->wp_config_path, $contents, LOCK_EX);
if (false === $result) {
throw new Exception('Failed to update the wp-config.php file.');
}
return true;
}
}

View File

@@ -0,0 +1,99 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Libs\WpConfig;
use Exception;
/**
* Transforms a wp-config.php file.
*/
class WPConfigTransformerSrc extends WPConfigTransformer
{
/**
* Instantiates the class with a valid wp-config.php scr text
*
* @param string $wp_config_src Path to a wp-config.php file.
*/
public function __construct($wp_config_src)
{
// Normalize the newline to prevent an issue coming from OSX
$this->wp_config_src = str_replace(array("\n\r", "\r"), array("\n", "\n"), $wp_config_src);
}
/**
* Get content string
*
* @return string
*/
public function getSrc()
{
return $this->wp_config_src;
}
/**
* Checks if a config exists in the wp-config.php src
*
* @throws Exception If the wp-config.php file is empty.
* @throws Exception If the requested config type is invalid.
*
* @param string $type Config type (constant or variable).
* @param string $name Config name.
*
* @return bool
*/
public function exists($type, $name)
{
$this->wp_configs = $this->parseWpConfig($this->wp_config_src);
if (!isset($this->wp_configs[$type])) {
throw new Exception("Config type '{$type}' does not exist.");
}
return isset($this->wp_configs[$type][$name]);
}
/**
* Get the value of a config in the wp-config.php src
*
* @param string $type Config type (constant or variable).
* @param string $name Config name.
* @param bool $get_real_value if true return typed value
*
* @return array
*/
public function getValue($type, $name, $get_real_value = true)
{
$this->wp_configs = $this->parseWpConfig($this->wp_config_src);
if (!isset($this->wp_configs[$type])) {
throw new Exception("Config type '{$type}' does not exist.");
}
// Duplicator Extra
$val = $this->wp_configs[$type][$name]['value'];
if ($get_real_value) {
return self::getRealValFromVal($val);
} else {
return $val;
}
}
/**
* Update wp_config_src
*
* @param string $contents config content
*
* @return boolean
*/
protected function save($contents)
{
$this->wp_config_src = $contents;
return true;
}
}

View File

@@ -0,0 +1,151 @@
<?php
/**
* Class that collects the functions of initial checks on the requirements to run the plugin
*
* Standard: PSR-2
*
* @link http://www.php-fig.org/psr/psr-2
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Lite;
class Requirements
{
const DUP_PRO_PLUGIN_KEY = 'duplicator-pro/duplicator-pro.php';
/**
*
* @var string // current plugin file full path
*/
protected static $pluginFile = '';
/**
*
* @var string // message on deactivation
*/
protected static $deactivationMessage = '';
/**
* This function checks the requirements to run Duplicator.
* At this point WordPress is not yet completely initialized so functionality is limited.
* It need to hook into "admin_init" to get the full functionality of WordPress.
*
* @param string $pluginFile // main plugin file path
*
* @return boolean // true if plugin can be executed
*/
public static function canRun($pluginFile)
{
$result = true;
self::$pluginFile = $pluginFile;
if ($result === true && self::isPluginActive(self::DUP_PRO_PLUGIN_KEY)) {
add_action('admin_init', array(__CLASS__, 'addProEnableNotice'));
$pluginUrl = (is_multisite() ? network_admin_url('plugins.php') : admin_url('plugins.php'));
self::$deactivationMessage = sprintf(
esc_html_x(
'Can\'t enable Duplicator LITE if the PRO version is enabled. Please deactivate Duplicator PRO,
then reactivate LITE version from the %1$splugins page%2$s.',
'%1$s and %2$s are <a> tags',
'duplicator'
),
'<a href="' . esc_url($pluginUrl) . '">',
'</a>'
);
$result = false;
}
if ($result === false) {
register_activation_hook($pluginFile, array(__CLASS__, 'deactivateOnActivation'));
}
return $result;
}
/**
*
* @param string $plugin plugin slug
*
* @return boolean return true if plugin key is active and plugin file exists
*/
protected static function isPluginActive($plugin)
{
$isActive = false;
if (in_array($plugin, (array) get_option('active_plugins', array()))) {
$isActive = true;
}
if (is_multisite()) {
$plugins = get_site_option('active_sitewide_plugins');
if (isset($plugins[$plugin])) {
$isActive = true;
}
}
return ($isActive && file_exists(WP_PLUGIN_DIR . '/' . $plugin));
}
/**
* display admin notice only if user can manage plugins.
*
* @return void
*/
public static function addProEnableNotice()
{
if (current_user_can('activate_plugins')) {
add_action('admin_notices', array(__CLASS__, 'proEnabledNotice'));
}
}
/**
* display admin notice
*
* @return void
*/
public static function addMultisiteNotice()
{
if (current_user_can('activate_plugins')) {
add_action('admin_notices', array(__CLASS__, 'multisiteNotice'));
}
}
/**
* deactivate current plugin on activation
*
* @return void
*/
public static function deactivateOnActivation()
{
deactivate_plugins(plugin_basename(self::$pluginFile));
wp_die(self::$deactivationMessage);
}
/**
* Display admin notice if duplicator pro is enabled
*
* @return void
*/
public static function proEnabledNotice()
{
$pluginUrl = (is_multisite() ? network_admin_url('plugins.php') : admin_url('plugins.php'));
?>
<div class="error notice">
<p>
<span class="dashicons dashicons-warning"></span>
<b><?php _e('Duplicator Notice:', 'duplicator'); ?></b>
<?php _e('The "Duplicator Lite" and "Duplicator Pro" plugins cannot both be active at the same time. ', 'duplicator'); ?>
</p>
<p>
<?php _e('To use "Duplicator LITE" please deactivate "Duplicator PRO" from the ', 'duplicator'); ?>
<a href="<?php echo esc_url($pluginUrl); ?>">
<?php _e('plugins page', 'duplicator'); ?>.
</a>
</p>
</div>
<?php
}
}

View File

@@ -0,0 +1,128 @@
<?php
/**
* Auloader calsses
*
* @package Duplicator
* @copyright (c) 2021, Snapcreek LLC
*/
namespace Duplicator\Utils;
/**
* Autoloader calss, dont user Duplicator library here
*/
final class Autoloader
{
const ROOT_NAMESPACE = 'Duplicator\\';
const ROOT_INSTALLER_NAMESPACE = 'Duplicator\\Installer\\';
protected static $nameSpacesMapping = null;
/**
* Register autoloader function
*
* @return void
*/
public static function register()
{
spl_autoload_register(array(__CLASS__, 'load'));
}
/**
* Load class
*
* @param string $className class name
*
* @return bool return true if class is loaded
*/
public static function load($className)
{
// @todo remove legacy logic in autoloading when duplicator is fully converted.
if (strpos($className, self::ROOT_NAMESPACE) !== 0) {
$legacyMappging = self::customLegacyMapping();
$legacyClass = strtolower(ltrim($className, '\\'));
if (array_key_exists($legacyClass, $legacyMappging)) {
if (file_exists($legacyMappging[$legacyClass])) {
include_once($legacyMappging[$legacyClass]);
return true;
}
}
if (self::externalLibs($className)) {
return true;
}
} else {
foreach (self::getNamespacesMapping() as $namespace => $mappedPath) {
if (strpos($className, $namespace) !== 0) {
continue;
}
$filepath = $mappedPath . str_replace('\\', '/', substr($className, strlen($namespace))) . '.php';
if (file_exists($filepath)) {
include_once($filepath);
return true;
}
}
}
return false;
}
/**
* Load external libs
*
* @param string $className class name
*
* @return bool return true if class is loaded
*/
protected static function externalLibs($className)
{
switch (strtolower(ltrim($className, '\\'))) {
default:
return false;
}
}
/**
* mappgin of some legacy classes
*
* @return array
*/
protected static function customLegacyMapping()
{
return array();
}
/**
* Return namespace mapping
*
* @return string[]
*/
protected static function getNamespacesMapping()
{
// the order is important, it is necessary to insert the longest namespaces first
return array(
self::ROOT_INSTALLER_NAMESPACE => DUPLICATOR_LITE_PATH . '/installer/dup-installer/src/',
self::ROOT_NAMESPACE => DUPLICATOR_LITE_PATH . '/src/'
);
}
/**
* Returns true if the $haystack string end with the $needle, only for internal use
*
* @param string $haystack The full string to search in
* @param string $needle The string to for
*
* @return bool Returns true if the $haystack string starts with the $needle
*/
protected static function endsWith($haystack, $needle)
{
$length = strlen($needle);
if ($length == 0) {
return true;
}
return (substr($haystack, -$length) === $needle);
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace Duplicator\Utils\CachesPurge;
use DUP_Log;
use Error;
use Exception;
class CacheItem
{
/**
* name of purge element (usualli plugin name)
*
* @var string
*/
protected $name = '';
/**
* check function, returns true if the element is to be purged
*
* @var callable|bool
*/
protected $checkCallback = null;
/**
* Purge cache callback
*
* @var callable
*/
protected $purgeCallback = null;
/**
* Message when cache is purged
*
* @var string
*/
protected $purgedMessage = '';
/**
* Construnctor
*
* @param string $name item name
* @param bool|callable $checkCallback check callback, return true if cache of current item have to removed
* @param callable $purgeCallback purge cache callback
*/
public function __construct($name, $checkCallback, $purgeCallback)
{
if (strlen($name) == 0) {
throw new Exception('name can\'t be empty');
}
$this->name = $name;
if (!is_bool($checkCallback) && !is_callable($checkCallback)) {
throw new Exception('checkCallback must be boolean or callable');
}
$this->checkCallback = $checkCallback;
/* purge callback may not exist if the referenced plugin is not initialized.
* That's why the check is performed only if you actually purge the plugin
*/
$this->purgeCallback = $purgeCallback;
$this->purgedMessage = sprintf(__('All caches on <b>%s</b> have been purged.', 'duplicator'), $this->name);
}
/**
* overwrite default purged message
*
* @param string $message message if item have benn purged
*
* @return void
*/
public function setPurgedMessage($message)
{
$this->purgedMessage = $message;
}
/**
* purge caches item
*
* @param string $message message if item have benn purged
*
* @return bool
*/
public function purge(&$message)
{
try {
if (
(is_bool($this->checkCallback) && $this->checkCallback) ||
call_user_func($this->checkCallback) == true
) {
DUP_Log::trace('Purge ' . $this->name);
if (!is_callable($this->purgeCallback)) {
throw new Exception('purgeCallback must be callable');
}
call_user_func($this->purgeCallback);
$message = $this->purgedMessage;
}
return true;
} catch (Exception $e) {
DUP_Log::trace('Error purge ' . $this->name . ' message:' . $e->getMessage());
$message = sprintf(__('Error on caches purge of <b>%s</b>.', 'duplicator'), $this->name);
return false;
} catch (Error $e) {
DUP_Log::trace('Error purge ' . $this->name . ' message:' . $e->getMessage());
$message = sprintf(__('Error on caches purge of <b>%s</b>.', 'duplicator'), $this->name);
return false;
}
}
}

View File

@@ -0,0 +1,291 @@
<?php
namespace Duplicator\Utils\CachesPurge;
class CachesPurge
{
/**
* purge all and return purge messages
*
* @return string[]
*/
public static function purgeAll()
{
$globalMessages = array();
$items = array_merge(
self::getPurgePlugins(),
self::getPurgeHosts()
);
foreach ($items as $item) {
$message = '';
$result = $item->purge($message);
if (strlen($message) > 0 && $result) {
$globalMessages[] = $message;
}
}
return $globalMessages;
}
/**
* get list to cache items to purge
*
* @return CacheItem[]
*/
protected static function getPurgePlugins()
{
$items = array();
$items[] = new CacheItem(
'Elementor',
function () {
return class_exists("\\Elementor\\Plugin");
},
function () {
\Elementor\Plugin::$instance->files_manager->clear_cache();
}
);
$items[] = new CacheItem(
'W3 Total Cache',
function () {
return function_exists('w3tc_pgcache_flush');
},
'w3tc_pgcache_flush'
);
$items[] = new CacheItem(
'WP Super Cache',
function () {
return function_exists('wp_cache_clear_cache');
},
'wp_cache_clear_cache'
);
$items[] = new CacheItem(
'WP Rocket',
function () {
return function_exists('rocket_clean_domain');
},
'rocket_clean_domain'
);
$items[] = new CacheItem(
'Fast velocity minify',
function () {
return function_exists('fvm_purge_static_files');
},
'fvm_purge_static_files'
);
$items[] = new CacheItem(
'Cachify',
function () {
return function_exists('cachify_flush_cache');
},
'cachify_flush_cache'
);
$items[] = new CacheItem(
'Comet Cache',
function () {
return class_exists('\\comet_cache');
},
array('\\comet_cache', 'clear')
);
$items[] = new CacheItem(
'Zen Cache',
function () {
return class_exists('\\zencache');
},
array('\\zencache', 'clear')
);
$items[] = new CacheItem(
'LiteSpeed Cache',
function () {
return has_action('litespeed_purge_all');
},
function () {
return do_action('litespeed_purge_all');
}
);
$items[] = new CacheItem(
'WP Cloudflare Super Page Cache',
function () {
return class_exists('\\SW_CLOUDFLARE_PAGECACHE');
},
function () {
return do_action("swcfpc_purge_everything");
}
);
$items[] = new CacheItem(
'Hyper Cache',
function () {
return class_exists('\\HyperCache');
},
function () {
return do_action('autoptimize_action_cachepurged');
}
);
$items[] = new CacheItem(
'Cache Enabler',
function () {
return has_action('ce_clear_cache');
},
function () {
return do_action('ce_clear_cache');
}
);
$items[] = new CacheItem(
'WP Fastest Cache',
function () {
return function_exists('wpfc_clear_all_cache');
},
function () {
wpfc_clear_all_cache(true);
}
);
$items[] = new CacheItem(
'Breeze',
function () {
return class_exists("\\Breeze_PurgeCache");
},
array('\\Breeze_PurgeCache', 'breeze_cache_flush')
);
$items[] = new CacheItem(
'Swift Performance',
function () {
return class_exists("\\Swift_Performance_Cache");
},
array('\\Swift_Performance_Cache', 'clear_all_cache')
);
$items[] = new CacheItem(
'Hummingbird',
function () {
return has_action('wphb_clear_page_cache');
},
function () {
return do_action('wphb_clear_page_cache');
}
);
$items[] = new CacheItem(
'WP-Optimize',
function () {
return has_action('wpo_cache_flush');
},
function () {
return do_action('wpo_cache_flush');
}
);
$items[] = new CacheItem(
'Wordpress default',
function () {
return function_exists('wp_cache_flush');
},
'wp_cache_flush'
);
$items[] = new CacheItem(
'Wordpress permalinks',
function () {
return function_exists('flush_rewrite_rules');
},
'flush_rewrite_rules'
);
return $items;
}
/**
* get list to cache items to purge
*
* @return CacheItem[]
*/
protected static function getPurgeHosts()
{
$items = array();
$items[] = new CacheItem(
'Godaddy Managed WordPress Hosting',
function () {
return class_exists('\\WPaaS\\Plugin') && method_exists('\\WPass\\Plugin', 'vip');
},
function () {
$method = 'BAN';
$url = home_url();
$host = wpraiser_get_domain();
$url = set_url_scheme(str_replace($host, \WPaas\Plugin::vip(), $url), 'http');
update_option('gd_system_last_cache_flush', time(), 'no'); # purge apc
wp_remote_request(
esc_url_raw($url),
array(
'method' => $method,
'blocking' => false,
'headers' =>
array(
'Host' => $host
)
)
);
}
);
$items[] = new CacheItem(
'SG Optimizer (Siteground)',
function () {
return function_exists('sg_cachepress_purge_everything');
},
'sg_cachepress_purge_everything'
);
$items[] = new CacheItem(
'WP Engine',
function () {
return (class_exists("\\WpeCommon") &&
(method_exists('\\WpeCommon', 'purge_memcached') ||
method_exists('\\WpeCommon', 'purge_varnish_cache')));
},
function () {
if (method_exists('\\WpeCommon', 'purge_memcached')) {
\WpeCommon::purge_memcached();
}
if (method_exists('\\WpeCommon', 'purge_varnish_cache')) {
\WpeCommon::purge_varnish_cache();
}
}
);
$items[] = new CacheItem(
'Kinsta',
function () {
global $kinsta_cache;
return (
(isset($kinsta_cache) &&
class_exists('\\Kinsta\\CDN_Enabler')) &&
!empty($kinsta_cache->kinsta_cache_purge));
},
function () {
global $kinsta_cache;
$kinsta_cache->kinsta_cache_purge->purge_complete_caches();
}
);
$items[] = new CacheItem(
'Pagely',
function () {
return class_exists('\\PagelyCachePurge');
},
function () {
$purge_pagely = new \PagelyCachePurge();
$purge_pagely->purgeAll();
}
);
$items[] = new CacheItem(
'Pressidum',
function () {
return defined('WP_NINUKIS_WP_NAME') && class_exists('\\Ninukis_Plugin');
},
function () {
$purge_pressidum = \Ninukis_Plugin::get_instance();
$purge_pressidum->purgeAllCaches();
}
);
$items[] = new CacheItem(
'Pantheon Advanced Page Cache plugin',
function () {
return function_exists('pantheon_wp_clear_edge_all');
},
'pantheon_wp_clear_edge_all'
);
return $items;
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Duplicator\Utils;
class CronUtils
{
const INTERVAL_DAILTY = 'duplicator_daily_cron';
const INTERVAL_WEEKLY = 'duplicator_weekly_cron';
const INTERVAL_MONTHLY = 'duplicator_monthly_cron';
/**
* Init WordPress hooks
*
* @return void
*/
public static function init()
{
add_filter('cron_schedules', array(__CLASS__, 'defaultCronIntervals'));
}
/**
* Add duplicator pro cron schedules
*
* @param array<string, array<string,int|string>> $schedules schedules
*
* @return array<string, array<string,int|string>>
*/
public static function defaultCronIntervals($schedules)
{
$schedules[self::INTERVAL_DAILTY] = array(
'interval' => DAY_IN_SECONDS,
'display' => __('Once a Day', 'duplicator'),
);
$schedules[self::INTERVAL_WEEKLY] = array(
'interval' => WEEK_IN_SECONDS,
'display' => __('Once a Week', 'duplicator'),
);
$schedules[self::INTERVAL_MONTHLY] = array(
'interval' => MONTH_IN_SECONDS,
'display' => __('Once a Month', 'duplicator'),
);
return $schedules;
}
}

View File

@@ -0,0 +1,112 @@
<?php
/**
* These functions are performed before including any other Duplicator file so
* do not use any Duplicator library or feature and use code compatible with PHP 5.2
*/
defined('ABSPATH') || exit;
// In the future it will be included on both PRO and LITE so you need to check if the define exists.
if (!class_exists('DuplicatorPhpVersionCheck')) {
class DuplicatorPhpVersionCheck // phpcs:ignore
{
/** @var string */
protected static $minVer = '';
/** @var string */
protected static $suggestedVer = '';
/**
* Check PhpVersin
*
* @param string $minVer min version of PHP
* @param string $suggestedVer suggested version of PHP
*
* @return bool
*/
public static function check($minVer, $suggestedVer)
{
self::$minVer = $minVer;
self::$suggestedVer = $suggestedVer;
if (version_compare(PHP_VERSION, self::$minVer, '<')) {
if (is_multisite()) {
add_action('network_admin_notices', array(__CLASS__, 'notice'));
} else {
add_action('admin_notices', array(__CLASS__, 'notice'));
}
return false;
} else {
return true;
}
}
/**
* Display notice
*
* @return void
*/
public static function notice()
{
if (preg_match('/^(\d+\.\d+(?:\.\d+)?)/', PHP_VERSION, $matches) === 1) {
$phpVersion = $matches[1];
} else {
$phpVersion = PHP_VERSION;
}
?>
<div class="error notice">
<p>
<?php
echo wp_kses(
sprintf(
__(
'DUPLICATOR: Action Required - <b>PHP Version Update Needed</b>, Your site is running PHP version %s.',
'duplicator'
),
esc_html($phpVersion)
),
[
'b' => [],
]
);
?><br><br>
<?php
echo wp_kses(
sprintf(
__(
'Starting from <b>Duplicator %1$s</b>, Duplicator will require <b>PHP %2$s or higher</b> to receive new updates.',
'duplicator'
),
'1.5.12',
esc_html(self::$minVer)
),
[
'b' => [],
]
);
?><br>
<?php
esc_html_e(
'While your current version of Duplicator will continue to work,
you\'ll need to upgrade your PHP version to receive future features, improvements, and security updates.',
'duplicator'
);
?><br>
<?php
esc_html_e(
'Please contact your hosting provider to upgrade your PHP version.',
'duplicator'
);
?>
</p>
<p>
<a href="https://duplicator.com/knowledge-base/updating-your-php-version-in-wordpress/" target="_blank">
<?php esc_html_e('Learn more about this change and how to upgrade', 'duplicator'); ?>
</a>
</p>
</div>
<?php
}
}
}

View File

@@ -0,0 +1,250 @@
<?php
namespace Duplicator\Utils\Email;
class EmailHelper
{
/** @var array<string, array<string, string>> List of styles in class => styles format*/
public static $styles = array(
'body' => array(
"border-collapse" => "collapse",
"border-spacing" => "0",
"vertical-align" => "top",
"mso-table-lspace" => "0pt",
"mso-table-rspace" => "0pt",
"-ms-text-size-adjust" => "100%",
"-webkit-text-size-adjust" => "100%",
"height" => "100% !important",
"width" => "100% !important",
"min-width" => "100%",
"-moz-box-sizing" => "border-box",
"-webkit-box-sizing" => "border-box",
"box-sizing" => "border-box",
"-webkit-font-smoothing" => "antialiased !important",
"-moz-osx-font-smoothing" => "grayscale !important",
"background-color" => "#e9eaec",
"color" => "#444444",
"font-family" => "'Helvetica Neue', Helvetica, Arial, sans-serif",
"font-weight" => "normal",
"padding" => "0",
"margin" => "0",
"text-align" => "left",
"font-size" => "14px",
"mso-line-height-rule" => "exactly",
"line-height" => "140%",
),
'table' => array(
"border-collapse" => "collapse",
"border-spacing" => "0",
"vertical-align" => "top",
"mso-table-lspace" => "0pt",
"mso-table-rspace" => "0pt",
"-ms-text-size-adjust" => "100%",
"-webkit-text-size-adjust" => "100%",
"margin" => "0 auto 0 auto",
"padding" => "0",
"text-align" => "inherit",
),
'main-tbl' => array(
"width" => "600px",
),
'stats-tbl' => array(
"-ms-text-size-adjust" => "100%",
"-webkit-text-size-adjust" => "100%",
"width" => "100%",
"margin" => "15px 0 38px 0",
),
'tr' => array(
"padding" => "0",
"vertical-align" => "top",
"text-align" => "left",
),
'td' => array(
"word-wrap" => "break-word",
"-webkit-hyphens" => "auto",
"-moz-hyphens" => "auto",
"hyphens" => "auto",
"border-collapse" => "collapse !important",
"vertical-align" => "top",
"mso-table-lspace" => "0pt",
"mso-table-rspace" => "0pt",
"-ms-text-size-adjust" => "100%",
"-webkit-text-size-adjust" => "100%",
"color" => "#444444",
"font-family" => "'Helvetica Neue', Helvetica, Arial, sans-serif",
"font-weight" => "normal",
"padding" => "0",
"margin" => "0",
"font-size" => "14px",
"mso-line-height-rule" => "exactly",
"line-height" => "140%",
),
'stats-count-cell' => array(
'width' => '1px',
'text-align' => 'center',
),
'unsubscribe' => array(
"padding" => "30px",
"color" => "#72777c",
"font-size" => "12px",
"text-align" => "center",
),
'th' => array(
"font-family" => "'Helvetica Neue', Helvetica, Arial, sans-serif",
"margin" => "0",
"text-align" => "left",
"font-size" => "14px",
"mso-line-height-rule" => "exactly",
"line-height" => "140%",
"font-weight" => "700",
"color" => "#777777",
"background" => "#f1f1f1",
"border" => "1px solid #f1f1f1",
"padding" => "17px 20px 17px 20px",
),
'stats-cell' => array(
"font-size" => "16px",
"border-top" => "none",
"border-right" => "none",
"border-bottom" => "1px solid #f1f1f1",
"border-left" => "none",
"color" => "#444444",
"padding" => "17px 20px 17px 20px",
),
'img' => array(
"outline" => "none",
"text-decoration" => "none",
"width" => "auto",
"clear" => "both",
"-ms-interpolation-mode" => "bicubic",
"display" => "inline-block !important",
"max-width" => "45%",
),
'h6' => array(
"padding" => "0",
"text-align" => "left",
"word-wrap" => "normal",
"font-family" => "'Helvetica Neue', Helvetica, Arial, sans-serif",
"font-weight" => "bold",
"mso-line-height-rule" => "exactly",
"line-height" => "130%",
"font-size" => "18px",
"color" => "#444444",
"margin" => "0 0 3px 0",
),
'p' => array(
"-ms-text-size-adjust" => "100%",
"-webkit-text-size-adjust" => "100%",
"font-family" => "'Helvetica Neue', Helvetica, Arial, sans-serif",
"font-weight" => "normal",
"padding" => "0",
"text-align" => "left",
"mso-line-height-rule" => "exactly",
"line-height" => "140%",
"overflow-wrap" => "break-word",
"word-wrap" => "break-word",
"-ms-word-break" => "break-all",
"word-break" => "break-word",
"-ms-hyphens" => "auto",
"-moz-hyphens" => "auto",
"-webkit-hyphens" => "auto",
"hyphens" => "auto",
"color" => "#777777",
"font-size" => "14px",
"margin" => "25px 0 25px 0",
),
'a' => array(
"-ms-text-size-adjust" => "100%",
"-webkit-text-size-adjust" => "100%",
"font-family" => "'Helvetica Neue', Helvetica, Arial, sans-serif",
"font-weight" => "normal",
"padding" => "0",
"margin" => "0",
"Margin" => "0",
"text-align" => "left",
"mso-line-height-rule" => "exactly",
"line-height" => "140%",
),
'footer-link' => array(
"color" => "#72777c",
"text-decoration" => "underline",
),
'inline-link' => array(
"color" => "inherit",
"text-decoration" => "underline",
),
'stats-title' => array(
"margin" => "0 0 15px 0",
),
'subtitle' => array(
"font-size" => "16px",
"margin" => "0 0 15px 0",
),
'txt-orange' => array(
"color" => "#e27730",
),
'txt-center' => array(
'text-align' => 'center',
),
'logo' => array(
"padding" => "30px 0px",
),
'content' => array(
"background-color" => "#ffffff",
"padding" => "60px 75px 45px 75px",
"border-top" => "3px solid #e27730",
"border-right" => "1px solid #dddddd",
"border-bottom" => "1px solid #dddddd",
"border-left" => "1px solid #dddddd",
),
'strong' => array(
"font-weight" => "bold",
),
);
/**
* Get Inline CSS of selector or empty if selector not found
*
* @param string $selectors Space separated selectors
*
* @return string
*/
public static function getStyle($selectors)
{
if ($selectors === '') {
return '';
}
$selArr = explode(' ', $selectors);
$uniqueStyles = array();
foreach ($selArr as $i => $selector) {
if (!isset(self::$styles[$selector])) {
continue;
}
//overwrite repeating styles
foreach (self::$styles[$selector] as $key => $value) {
$uniqueStyles[$key] = $value;
}
}
$style = '';
foreach ($uniqueStyles as $key => $value) {
$style .= $key . ': ' . $value . ';';
}
return $style;
}
/**
* Print Inline CSS of selector or empty if selector not found
*
* @param string $selectors Space separated selectors
*
* @return void
*/
public static function printStyle($selectors)
{
echo 'class="' . esc_attr($selectors) . '" style="' . self::getStyle($selectors) . '"';
}
}

View File

@@ -0,0 +1,171 @@
<?php
namespace Duplicator\Utils\Email;
use DUP_Package;
use DUP_Settings;
use DUP_PackageStatus;
use Duplicator\Core\Controllers\ControllersManager;
use Duplicator\Libs\Snap\JsonSerialize\JsonSerialize;
/**
* Email Summary
*/
class EmailSummary
{
const SEND_FREQ_NEVER = 'never';
const SEND_FREQ_DAILY = 'daily';
const SEND_FREQ_WEEKLY = 'weekly';
const SEND_FREQ_MONTHLY = 'monthly';
/**
* Old option key for storing email summary info, used for migrating data to the correct option key
*/
const INFO_OPT_OLD_KEY = 'duplicator-email-summary-info';
const PEVIEW_SLUG = 'duplicator-email-summary-preview';
const INFO_OPT_KEY = 'duplicator_email_summary_info';
/** @var self The singleton instance */
private static $self = null;
/** @var int[] Manual package ids */
private $manualPackageIds = array();
/** @var int[] info about created storages*/
private $failedPackageIds = array();
/**
* Get the singleton instance
*
* @return self
*/
public static function getInstance()
{
if (self::$self == null) {
self::$self = new self();
}
return self::$self;
}
/**
* Create Email Summary object
*/
private function __construct()
{
if (($data = get_option(self::INFO_OPT_KEY)) !== false) {
JsonSerialize::unserializeToObj($data, $this);
}
}
/**
* Returns the preview link
*
* @return string
*/
public static function getPreviewLink()
{
return ControllersManager::getMenuLink(self::PEVIEW_SLUG);
}
/**
* Add package to summary
*
* @param DUP_Package $package The package
* @param int $status The status
*
* @return void
*/
public function addPackage(DUP_Package $package, $status)
{
if ($status !== DUP_PackageStatus::COMPLETE && $status !== DUP_PackageStatus::ERROR) {
return;
}
if ($status === DUP_PackageStatus::COMPLETE) {
$this->manualPackageIds[] = $package->ID;
} elseif ($status === DUP_PackageStatus::ERROR) {
$this->failedPackageIds[] = $package->ID;
}
$this->save();
}
/**
* Returns info about created packages
*
* @return array<int|string, array<string, string|int>>
*/
public function getPackagesInfo()
{
$packagesInfo = array();
$packagesInfo['manual'] = array(
'name' => __('Successful', 'duplicator'),
'count' => count($this->manualPackageIds),
);
$packagesInfo['failed'] = array(
'name' => __('Failed', 'duplicator'),
'count' => count($this->failedPackageIds),
);
return $packagesInfo;
}
/**
* Get all frequency options
*
* @return array<int, string>
*/
public static function getAllFrequencyOptions()
{
return array(
self::SEND_FREQ_NEVER => esc_html__('Never', 'duplicator'),
self::SEND_FREQ_DAILY => esc_html__('Daily', 'duplicator'),
self::SEND_FREQ_WEEKLY => esc_html__('Weekly', 'duplicator'),
self::SEND_FREQ_MONTHLY => esc_html__('Monthly', 'duplicator'),
);
}
/**
* Get the frequency text displayed in the email
*
* @return string
*/
public static function getFrequencyText()
{
$frequency = DUP_Settings::Get('email_summary_frequency');
switch ($frequency) {
case self::SEND_FREQ_DAILY:
return esc_html__('day', 'duplicator');
case self::SEND_FREQ_MONTHLY:
return esc_html__('month', 'duplicator');
case self::SEND_FREQ_WEEKLY:
default:
return esc_html__('week', 'duplicator');
}
}
/**
* Reset plugin data
*
* @return bool True if data has been reset, false otherwise
*/
public function resetData()
{
$this->manualPackageIds = array();
$this->failedPackageIds = array();
return $this->save();
}
/**
* Save plugin data
*
* @return bool True if data has been saved, false otherwise
*/
private function save()
{
return update_option(self::INFO_OPT_KEY, JsonSerialize::serialize($this));
}
}

View File

@@ -0,0 +1,249 @@
<?php
namespace Duplicator\Utils\Email;
use DUP_Log;
use DUP_Package;
use DUP_Settings;
use DUP_PackageStatus;
use Duplicator\Utils\CronUtils;
use Duplicator\Libs\Snap\SnapWP;
use Duplicator\Core\Views\TplMng;
/**
* Email summary bootstrap
*/
class EmailSummaryBootstrap
{
const CRON_HOOK = 'duplicator_email_summary_cron';
/**
* Init Email Summaries
*
* @return void
*/
public static function init()
{
//Package hooks
add_action('duplicator_package_after_set_status', array(__CLASS__, 'addPackage'), 10, 2);
//Set cron action
add_action(self::CRON_HOOK, array(__CLASS__, 'send'));
//Activation/deactivation hooks
add_action('duplicator_after_activation', array(__CLASS__, 'activationAction'));
add_action('duplicator_after_deactivation', array(__CLASS__, 'deactivationAction'));
}
/**
* Add package to summary
*
* @param DUP_Package $package The package
* @param int $status The status
*
* @return void
*/
public static function addPackage(DUP_Package $package, $status)
{
EmailSummary::getInstance()->addPackage($package, $status);
}
/**
* Send email summary
*
* @return bool True if email was sent
*/
public static function send()
{
$frequency = DUP_Settings::Get('email_summary_frequency');
if (($recipient = get_option('admin_email')) === false || $frequency === EmailSummary::SEND_FREQ_NEVER) {
return false;
}
$parsedHomeUrl = wp_parse_url(home_url());
$siteDomain = $parsedHomeUrl['host'];
if (is_multisite() && isset($parsedHomeUrl['path'])) {
$siteDomain .= $parsedHomeUrl['path'];
}
$subject = sprintf(
esc_html_x(
'Your Weekly Duplicator Summary for %s',
'%s is the site domain',
'duplicator'
),
$siteDomain
);
$content = TplMng::getInstance()->render('mail/email_summary', array(
'packages' => EmailSummary::getInstance()->getPackagesInfo(),
), false);
add_filter('wp_mail_content_type', array(__CLASS__, 'getMailContentType'));
if (!wp_mail($recipient, $subject, $content)) {
DUP_Log::Trace("FAILED TO SEND EMAIL SUMMARY.");
DUP_Log::Trace("Recipients: " . $recipient);
return false;
} elseif (!EmailSummary::getInstance()->resetData()) {
DUP_Log::Trace("FAILED TO RESET EMAIL SUMMARY DATA.");
return false;
}
return true;
}
/**
* Get mail content type
*
* @return string
*/
public static function getMailContentType()
{
return 'text/html';
}
/**
* Activation action
*
* @return void
*/
public static function activationAction()
{
$frequency = DUP_Settings::Get('email_summary_frequency');
if ($frequency === EmailSummary::SEND_FREQ_NEVER) {
return;
}
if (self::updateCron($frequency) == false) {
DUP_Log::Trace("FAILED TO INIT EMAIL SUMMARY CRON. Frequency: {$frequency}");
}
}
/**
* Deactivation action
*
* @return void
*/
public static function deactivationAction()
{
if (self::updateCron(EmailSummary::SEND_FREQ_NEVER) == false) {
DUP_Log::Trace("FAILED TO REMOVE EMAIL SUMMARY CRON.");
}
}
/**
* Update next send time on frequency setting change
*
* @param string $oldFrequency The old frequency
* @param string $newFrequency The new frequency
*
* @return bool True if the cron was updated or false on error
*/
public static function updateFrequency($oldFrequency, $newFrequency)
{
if ($oldFrequency === $newFrequency) {
return true;
}
return self::updateCron($newFrequency);
}
/**
* Updates the WP Cron job base on frequency or settings
*
* @param string $frequency The frequency
*
* @return bool True if the cron was updated or false on error
*/
private static function updateCron($frequency = '')
{
if (strlen($frequency) === 0) {
$frequency = DUP_Settings::Get('email_summary_frequency');
}
if ($frequency === EmailSummary::SEND_FREQ_NEVER) {
if (wp_next_scheduled(self::CRON_HOOK)) {
//have to check return like this because
//wp_clear_scheduled_hook returns void in WP < 5.1
return !self::isFalseOrWpError(wp_clear_scheduled_hook(self::CRON_HOOK));
} else {
return true;
}
} else {
if (
wp_next_scheduled(self::CRON_HOOK)
&& self::isFalseOrWpError(wp_clear_scheduled_hook(self::CRON_HOOK))
) {
return false;
}
return !self::isFalseOrWpError(wp_schedule_event(
self::getFirstRunTime($frequency),
self::getCronSchedule($frequency),
self::CRON_HOOK
));
}
}
/**
* Set next send time based on frequency
*
* @param string $frequency Frequency
*
* @return int
*/
private static function getFirstRunTime($frequency)
{
switch ($frequency) {
case EmailSummary::SEND_FREQ_DAILY:
$firstRunTime = strtotime('tomorrow 14:00');
break;
case EmailSummary::SEND_FREQ_WEEKLY:
$firstRunTime = strtotime('next monday 14:00');
break;
case EmailSummary::SEND_FREQ_MONTHLY:
$firstRunTime = strtotime('first day of next month 14:00');
break;
case EmailSummary::SEND_FREQ_NEVER:
return 0;
default:
throw new \Exception("Unknown frequency: " . $frequency);
}
return $firstRunTime - SnapWP::getGMTOffset();
}
/**
* Get the cron schedule
*
* @param string $frequency The frequency
*
* @return string
*/
private static function getCronSchedule($frequency)
{
switch ($frequency) {
case EmailSummary::SEND_FREQ_DAILY:
return CronUtils::INTERVAL_DAILTY;
case EmailSummary::SEND_FREQ_WEEKLY:
return CronUtils::INTERVAL_WEEKLY;
case EmailSummary::SEND_FREQ_MONTHLY:
return CronUtils::INTERVAL_MONTHLY;
default:
throw new Exception("Unknown frequency: " . $frequency);
}
}
/**
* Returns true if is false or wp_error
*
* @param mixed $value The value
*
* @return bool
*/
private static function isFalseOrWpError($value)
{
return $value === false || is_wp_error($value);
}
}

View File

@@ -0,0 +1,164 @@
<?php
/**
* Expire options
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Utils;
use Duplicator\Libs\Snap\JsonSerialize\JsonSerialize;
use Duplicator\Libs\Snap\SnapDB;
final class ExpireOptions
{
const OPTION_PREFIX = 'duplicator_expire_';
/** @var array<string, array{expire: int, value: mixed}> */
private static $cacheOptions = array();
/**
* Sets/updates the value of a expire option.
*
* You do not need to serialize values. If the value needs to be serialized,
* then it will be serialized before it is set.
*
* @param string $key Expire option key.
* @param mixed $value Option value.
* @param int $expiration Optional. Time until expiration in seconds. Default 0 (no expiration).
*
* @return bool True if the value was set, false otherwise.
*/
public static function set($key, $value, $expiration = 0)
{
$time = ($expiration > 0 ? time() + $expiration : 0);
self::$cacheOptions[$key] = array(
'expire' => $time,
'value' => $value,
);
return update_option(self::OPTION_PREFIX . $key, JsonSerialize::serialize(self::$cacheOptions[$key]), true);
}
/**
* Retrieves the value of a expire option.
*
* If the option does not exist, does not have a value, or has expired,
* then the return value will be false.
*
* @param string $key Expire option key.
* @param mixed $default Return this value if option don\'t exists os is expired
*
* @return mixed Value of transient.
*/
public static function get($key, $default = false)
{
if (!isset(self::$cacheOptions[$key])) {
if (($option = get_option(self::OPTION_PREFIX . $key)) == false) {
self::$cacheOptions[$key] = self::unexistsKeyValue();
} else {
self::$cacheOptions[$key] = JsonSerialize::unserialize($option);
}
}
if (self::$cacheOptions[$key]['expire'] < 0) {
// don't exists the wp-option
return $default;
}
if (self::$cacheOptions[$key]['expire'] > 0 && self::$cacheOptions[$key]['expire'] < time()) {
// if 0 don't expire so check only if time is > 0
self::delete($key);
return $default;
}
return self::$cacheOptions[$key]['value'];
}
/**
* This function returns the value of the option or false if it has expired. In case the option has expired then it is updated.
* It does the same thing as a get and a set but with one less query.
*
* @param string $key Expire option key.
* @param mixed $value Option value.
* @param int $expiration Optional. Time until expiration in seconds. Default 0 (no expiration).
*
* @return mixed Value of transient.
*/
public static function getUpdate($key, $value, $expiration = 0)
{
if (!isset(self::$cacheOptions[$key])) {
if (($option = get_option(self::OPTION_PREFIX . $key)) == false) {
self::$cacheOptions[$key] = self::unexistsKeyValue();
} else {
self::$cacheOptions[$key] = JsonSerialize::unserialize($option);
}
}
if (self::$cacheOptions[$key]['expire'] < time()) {
self::set($key, $value, $expiration);
return false;
}
return self::$cacheOptions[$key]['value'];
}
/**
* Deletes a option
*
* @param string $key Expire option key. Expected to not be SQL-escaped.
*
* @return bool True if the option was deleted, false otherwise.
*/
public static function delete($key)
{
if (delete_option(self::OPTION_PREFIX . $key)) {
self::$cacheOptions[$key] = self::unexistsKeyValue();
return true;
} else {
return false;
}
}
/**
* Delete all options
*
* @return bool
*/
public static function deleteAll()
{
/** @var \wpdb $wpdb */
global $wpdb;
$optionsTableName = esc_sql($wpdb->base_prefix . "options");
$query = $wpdb->prepare(
"SELECT `option_name` FROM `{$optionsTableName}` WHERE `option_name` REGEXP %s",
SnapDB::quoteRegex(self::OPTION_PREFIX)
);
$dupOptionNames = $wpdb->get_col($query);
foreach ($dupOptionNames as $dupOptionName) {
delete_option($dupOptionName);
}
self::$cacheOptions = array();
return true;
}
/**
* Return value for unexists key option
*
* @return array{expire: int, value: false}
*/
private static function unexistsKeyValue()
{
return array(
'expire' => -1,
'value' => false,
);
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace Duplicator\Utils\ExtraPlugins;
use DUP_LITE_Plugin_Upgrade;
use Duplicator\Controllers\AboutUsController;
use Duplicator\Core\Controllers\ControllersManager;
use Duplicator\Core\Notifications\Notice;
use Duplicator\Core\Views\TplMng;
class CrossPromotion
{
const PLUGINS_LIMIT = 3;
/** @var string */
const NOTICE_SLUG = 'duplicator_cross_promotion';
/**
* Init notice
*
* @return void
*/
public static function init()
{
if (!current_user_can('install_plugins')) {
return;
}
$installInfo = DUP_LITE_Plugin_Upgrade::getInstallInfo();
if ($installInfo['updateTime'] + (2 * WEEK_IN_SECONDS) > time()) {
return;
}
if (!ControllersManager::isCurrentPage(ControllersManager::MAIN_MENU_SLUG)) {
return;
}
$plugins = self::getExtraPlugins();
if (count($plugins) === 0) {
return;
}
AboutUsController::enqueueScripts();
Notice::add(
TplMng::getInstance()->render(
'parts/cross_promotion/list',
[
'plugins' => $plugins,
'limit' => self::PLUGINS_LIMIT,
],
false
),
self::NOTICE_SLUG,
'',
[
'autop' => false,
'dismiss' => Notice::DISMISS_USER,
]
);
}
/**
* Get the extra plugins to be promoted
*
* @return ExtraItem[]
*/
public static function getExtraPlugins()
{
$slugs = self::getSlugs();
$plugins = [];
$extraPluginsMng = ExtraPluginsMng::getInstance();
foreach ($slugs as $slug) {
if (count($plugins) >= self::PLUGINS_LIMIT) {
break;
}
if (($plugin = $extraPluginsMng->getBySlug($slug)) === false) {
continue;
}
if ($plugin->isInstalled() || !$plugin->checkRequirments()) {
continue;
}
$plugins[] = $plugin;
}
foreach ($extraPluginsMng->getAll() as $plugin) {
if (count($plugins) >= self::PLUGINS_LIMIT) {
break;
}
if (in_array($plugin->getSlug(), $slugs)) {
continue;
}
if ($plugin->isInstalled() || !$plugin->checkRequirments()) {
continue;
}
$plugins[] = $plugin;
}
return $plugins;
}
/**
* Get the slugs of the extra plugins to be promoted with priority
*
* @return string[]
*/
protected static function getSlugs()
{
return [
'search-replace-wpcode/wsrw.php',
'wp-mail-smtp/wp_mail_smtp.php',
'insert-headers-and-footers/ihaf.php',
'all-in-one-seo-pack/all_in_one_seo_pack.php',
'wpforms-lite/wpforms.php',
'uncanny-automator/uncanny-automator.php',
];
}
}

View File

@@ -0,0 +1,364 @@
<?php
namespace Duplicator\Utils\ExtraPlugins;
use DUP_Log;
use Duplicator\Libs\Snap\SnapString;
use Duplicator\Utils\ExpireOptions;
class ExtraItem
{
const STATUS_NOT_INSTALLED = 0;
const STATUS_INSTALLED = 1;
const STATUS_ACTIVE = 2;
const URL_TYPE_GENERIC = 0;
const URL_TYPE_ZIP = 1;
const PLUGIN_API_FIELDS = [
'active_installs' => false,
'added' => false,
'author' => false,
'author_block_count' => false,
'author_block_rating' => false,
'author_profile' => false,
'banners' => false,
'compatibility' => false,
'contributors' => false,
'description' => false,
'donate_link' => false,
'download_link' => false,
'downloaded' => false,
'group' => false,
'homepage' => false,
'icons' => false,
'last_updated' => false,
'name' => false,
'num_ratings' => false,
'rating' => false,
'ratings' => false,
'requires' => true,
'requires_php' => true,
'reviews' => false,
'screenshots' => false,
'sections' => false,
'short_description' => false,
'slug' => false,
'support_threads' => false,
'support_threads_resolved' => false,
'tags' => false,
'tested' => false,
'version' => false,
'versions' => false,
];
/**
* plugin name
*
* @var string
*/
public $name = '';
/**
* plugin slug
*
* @var string
*/
protected $slug = '';
/**
* url to plugin icon
*
* @var string
*/
public $icon = '';
/**
* plugin description
*
* @var string
*/
public $desc = '';
/**
* plugin url either to zip file or pro version
*
* @var string
*/
public $url = '';
/**
* plugin url on wordpress.org if available
*
* @var bool|string
*/
public $wpOrgURL = '';
/**
* PRO version of plugin if available
*
* @var ExtraItem|null
*/
protected $pro = null;
/**
* Class constructor
*
* @param string $name plugin name
* @param string $slug plugin slug
* @param string $icon url to plugin icon
* @param string $desc plugin description
* @param string $url plugin url
* @param string|bool $wpOrgURL plugin url on wordpress.org
*/
public function __construct($name, $slug, $icon, $desc, $url, $wpOrgURL = false)
{
$this->name = $name;
$this->slug = $slug;
$this->icon = $icon;
$this->desc = $desc;
$this->url = $url;
$this->wpOrgURL = $wpOrgURL;
}
/**
* Returns plugin slug
*
* @return string
*/
public function getSlug()
{
return $this->slug;
}
/**
* Is plugin active
*
* @return bool
*/
public function isActive()
{
return $this->isInstalled() && is_plugin_active($this->slug);
}
/**
* Is plugin installed
*
* @return bool
*/
public function isInstalled()
{
static $installedSlugs = null;
if ($installedSlugs === null) {
if (!function_exists('get_plugins')) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$installedSlugs = array_keys(get_plugins());
}
return in_array($this->slug, $installedSlugs);
}
/**
* Checks of the WP and PHP version requirments pass
*
* @return bool True if checks pass, false otherwise
*/
public function checkRequirments()
{
global $wp_version;
if (($reqs = $this->getRequirments()) === false) {
return false;
}
return version_compare($wp_version, $reqs['wp_version'], '>=') &&
version_compare(PHP_VERSION, $reqs['php_version'], '>=');
}
/**
* Gets the requirments either from the cache (wp_options) or from the remote
* API and updates the cache.
*
* @return array{last_updated:int,wp_version:string,php_version:string}|false The data or false of failure
*/
private function getRequirments()
{
if (($data = ExpireOptions::get($this->getApiSlug())) === false) {
if (($data = $this->getRemoteRequirments()) !== false) {
ExpireOptions::set($this->getApiSlug(), $data, 2 * WEEK_IN_SECONDS);
} else {
return false;
}
}
return $data;
}
/**
* Returns the slug that should be used with the API calls
*
* @return string
*/
private function getApiSlug()
{
return dirname($this->getSlug());
}
/**
* Retrieves the PHP and WP version requirments of a plugin from the WP API.
*
* @return array{wp_version:string,php_version:string}|false The data or false on failure
*/
private function getRemoteRequirments()
{
if (!function_exists('plugins_api')) {
require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
}
$response = plugins_api('plugin_information', [
'slug' => $this->getApiSlug(),
'fields' => self::PLUGIN_API_FIELDS
]);
if (is_wp_error($response)) {
return false;
}
return [
'wp_version' => $response->requires,
'php_version' => $response->requires_php
];
}
/**
* Returns pro version of plugin if available
*
* @return ExtraItem|null
*/
public function getPro()
{
return $this->pro;
}
/**
* Set pro plugin
*
* @param string $name plugin name
* @param string $slug plugin slug
* @param string $icon url to plugin icon
* @param string $desc plugin description
* @param string $url plugin url
* @param string|bool $wpOrgURL plugin url on wordpress.org
*
* @return void
*/
public function setPro($name, $slug, $icon, $desc, $url, $wpOrgURL = false)
{
$this->pro = new self($name, $slug, $icon, $desc, $url, $wpOrgURL);
}
/**
* Whether to skip lite version of plugin because it is installed and pro version is available
*
* @return bool
*/
public function skipLite()
{
return $this->pro !== null && $this->isActive();
}
/**
* Enum of status constants (STATUS_ACTIVE, STATUS_INSTALLED, STATUS_UNINSALED)
*
* @return int return status constant
*/
public function getStatus()
{
if ($this->isActive()) {
return self::STATUS_ACTIVE;
} elseif ($this->isInstalled()) {
return self::STATUS_INSTALLED;
} else {
return self::STATUS_NOT_INSTALLED;
}
}
/**
* Status text
*
* @return string
*/
public function getStatusText()
{
switch ($this->getStatus()) {
case self::STATUS_ACTIVE:
return __('Active', 'duplicator');
case self::STATUS_INSTALLED:
return __('Inactive', 'duplicator');
case self::STATUS_NOT_INSTALLED:
return __('Not Installed', 'duplicator');
}
}
/**
* Enum of URL constants (URL_TYPE_GENERIC, URL_TYPE_ZIP)
*
* @return int
*/
public function getURLType()
{
if (SnapString::endsWith($this->url, '.zip')) {
return self::URL_TYPE_ZIP;
} else {
return self::URL_TYPE_GENERIC;
}
}
/**
* Install this plugin
*
* @return bool true on success
*/
public function install()
{
if ($this->isInstalled()) {
return true;
}
if (!SnapString::endsWith($this->url, '.zip')) {
throw new \Exception('Invalid plugin url for installation');
}
if (!current_user_can('install_plugins')) {
throw new \Exception('User does not have permission to install plugins');
}
if (!class_exists('Plugin_Upgrader')) {
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
}
wp_cache_flush();
$upgrader = new \Plugin_Upgrader(new \Automatic_Upgrader_Skin());
if (!$upgrader->install($this->url)) {
throw new \Exception('Failed to install plugin');
}
return true;
}
/**
* Activate this plugin
*
* @return bool true on success
*/
public function activate()
{
if ($this->isActive()) {
return true;
}
if (!is_null(activate_plugin($this->slug))) {
throw new \Exception('Failed to activate plugin');
}
return true;
}
}

View File

@@ -0,0 +1,549 @@
<?php
namespace Duplicator\Utils\ExtraPlugins;
final class ExtraPluginsMng
{
/** @var ?self */
private static $instance = null;
/** @var array<string, ExtraItem> key slug item */
protected $plugins = array();
/**
*
* @return self
*/
public static function getInstance()
{
if (is_null(self::$instance)) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Singleton constructor
*/
protected function __construct()
{
$this->plugins = self::getInitList();
}
/**
* Execute callback for each plugin
*
* @param callable $callback callback function
*
* @return void
*/
public function foreachCallback($callback)
{
if (!is_callable($callback)) {
return;
}
foreach ($this->plugins as $plugin) {
call_user_func($callback, $plugin);
}
}
/**
* Get all plugins
*
* @return ExtraItem[] All plugin items
*/
public function getAll()
{
return $this->plugins;
}
/**
* Returns plugin by slug
*
* @param string $slug plugin slug
*
* @return false|ExtraItem plugin item or false if not found
*/
public function getBySlug($slug)
{
if (strlen($slug) === 0) {
return false;
}
if (!isset($this->plugins[$slug])) {
return false;
}
return (isset($this->plugins[$slug]) ? $this->plugins[$slug] : false);
}
/**
* Install plugin slug
*
* @param string $slug plugin slug
* @param string $message message
*
* @return bool true if plugin installed and activated or false on failure
*/
public function install($slug, &$message = '')
{
if (strlen($slug) === 0) {
$message = __('Plugin slug is empty', 'duplicator');
return false;
}
if (($plugin = $this->getBySlug($slug)) == false) {
$message = __('Plugin not found', 'duplicator');
return false;
}
$result = true;
ob_start();
if ($plugin->install() == false) {
$result = false;
} elseif ($plugin->activate() == false) {
$result = false;
}
$message = ob_get_clean();
return $result;
}
/**
* Init addon plugins
*
* @return void
*/
private static function getInitList()
{
$result = array();
$item = new ExtraItem(
__('OptinMonster', 'duplicator'),
'optinmonster/optin-monster-wp-api.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-om.png',
__('Instantly get more subscribers, leads, and sales with the #1 conversion optimization toolkit. Create ' .
'high converting popups, announcement bars, spin a wheel, and more with smart targeting and personalization.', 'duplicator'),
'https://downloads.wordpress.org/plugin/optinmonster.zip',
'https://wordpress.org/plugins/optinmonster/'
);
$result[$item->getSlug()] = $item;
$item = new ExtraItem(
__('MonsterInsights', 'duplicator'),
'google-analytics-for-wordpress/googleanalytics.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-mi.png',
__(
'The leading WordPress analytics plugin that shows you how people find and use your website, so you can ' .
'make data driven decisions to grow your business. Properly set up Google Analytics without writing code.',
'duplicator'
),
'https://downloads.wordpress.org/plugin/google-analytics-for-wordpress.zip',
'https://wordpress.org/plugins/google-analytics-for-wordpress/'
);
$item->setPro(
__('MonsterInsights Pro', 'duplicator'),
'google-analytics-premium/googleanalytics-premium.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-mi.png',
__(
'The leading WordPress analytics plugin that shows you how people find and use your website, so you ' .
'can make data driven decisions to grow your business. Properly set up Google Analytics without writing code.',
'duplicator'
),
'https://www.monsterinsights.com/?utm_source=duplicatorplugin&utm_medium=link&utm_campaign=About%20Duplicator'
);
$result[$item->getSlug()] = $item;
$item = new ExtraItem(
__('WPForms', 'duplicator'),
'wpforms-lite/wpforms.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-wpforms.png',
__(
'The best drag & drop WordPress form builder. Easily create beautiful contact forms, surveys, payment ' .
'forms, and more with our 100+ form templates. Trusted by over 4 million websites as the best forms plugin.',
'duplicator'
),
'https://downloads.wordpress.org/plugin/wpforms-lite.zip',
'https://wordpress.org/plugins/wpforms-lite/'
);
$item->setPro(
__('WPForms Pro', 'duplicator'),
'wpforms/wpforms.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-wpforms.png',
__(
'The easiest drag & drop WordPress form builder plugin to create beautiful contact forms, subscription ' .
'forms, payment forms, and more in minutes. No coding skills required.',
'duplicator'
),
'https://wpforms.com/?utm_source=duplicatorplugin&utm_medium=link&utm_campaign=About%20Duplicator'
);
$result[$item->getSlug()] = $item;
$item = new ExtraItem(
__('WP Mail SMTP', 'duplicator'),
'wp-mail-smtp/wp_mail_smtp.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-smtp.png',
__(
'Improve your WordPress email deliverability and make sure that your website emails reach user\'s inbox ' .
'with the #1 SMTP plugin for WordPress. Over 3 million websites use it to fix WordPress email issues.',
'duplicator'
),
'https://downloads.wordpress.org/plugin/wp-mail-smtp.zip',
'https://wordpress.org/plugins/wp-mail-smtp/'
);
$item->setPro(
__('WP Mail SMTP Pro', 'duplicator'),
'wp-mail-smtp-pro/wp_mail_smtp.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-smtp.png',
__(
'Improve your WordPress email deliverability and make sure that your website emails reach user\'s inbox ' .
'with the #1 SMTP plugin for WordPress. Over 3 million websites use it to fix WordPress email issues.',
'duplicator'
),
'https://wpmailsmtp.com/?utm_source=duplicatorplugin&utm_medium=link&utm_campaign=About%20Duplicator'
);
$result[$item->getSlug()] = $item;
$item = new ExtraItem(
__('AIOSEO', 'duplicator'),
'all-in-one-seo-pack/all_in_one_seo_pack.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-aioseo.png',
__(
'The original WordPress SEO plugin and toolkit that improves your website\'s search rankings. Comes with ' .
'all the SEO features like Local SEO, WooCommerce SEO, sitemaps, SEO optimizer, schema, and more.',
'duplicator'
),
'https://downloads.wordpress.org/plugin/all-in-one-seo-pack.zip',
'https://wordpress.org/plugins/all-in-one-seo-pack/'
);
$item->setPro(
__('AIOSEO Pro', 'duplicator'),
'all-in-one-seo-pack-pro/all_in_one_seo_pack.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-aioseo.png',
__(
'The original WordPress SEO plugin and toolkit that improves your website\'s search rankings. Comes ' .
'with all the SEO features like Local SEO, WooCommerce SEO, sitemaps, SEO optimizer, schema, and more.',
'duplicator'
),
'https://aioseo.com/?utm_source=duplicatorplugin&utm_medium=link&utm_campaign=About%20Duplicator'
);
$result[$item->getSlug()] = $item;
$item = new ExtraItem(
__('SeedProd', 'duplicator'),
'coming-soon/coming-soon.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-seedprod.png',
__('The best WordPress coming soon page plugin to create a beautiful coming soon page, maintenance mode page, ' .
'or landing page. No coding skills required.', 'duplicator'),
'https://downloads.wordpress.org/plugin/coming-soon.zip',
'https://wordpress.org/plugins/coming-soon/'
);
$item->setPro(
__('SeedProd Pro', 'duplicator'),
'seedprod-coming-soon-pro-5/seedprod-coming-soon-pro-5.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-seedprod.png',
__('The best WordPress coming soon page plugin to create a beautiful coming soon page, maintenance mode ' .
'page, or landing page. No coding skills required.', 'duplicator'),
'https://www.seedprod.com/?utm_source=duplicatorplugin&utm_medium=link&utm_campaign=About%20Duplicator'
);
$result[$item->getSlug()] = $item;
$item = new ExtraItem(
__('RafflePress', 'duplicator'),
'rafflepress/rafflepress.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-rp.png',
__(
'Turn your website visitors into brand ambassadors! Easily grow your email list, website traffic, and social ' .
'media followers with the most powerful giveaways & contests plugin for WordPress.',
'duplicator'
),
'https://downloads.wordpress.org/plugin/rafflepress.zip',
'https://wordpress.org/plugins/rafflepress/'
);
$item->setPro(
__('RafflePress Pro', 'duplicator'),
'rafflepress-pro/rafflepress-pro.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-rp.png',
__(
'Turn your website visitors into brand ambassadors! Easily grow your email list, website traffic, and ' .
'social media followers with the most powerful giveaways & contests plugin for WordPress.',
'duplicator'
),
'https://rafflepress.com/?utm_source=duplicatorplugin&utm_medium=link&utm_campaign=About%20Duplicator'
);
$result[$item->getSlug()] = $item;
$item = new ExtraItem(
__('PushEngage', 'duplicator'),
'pushengage/main.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-pushengage.png',
__(
'Connect with your visitors after they leave your website with the leading web push notification software. ' .
'Over 10,000+ businesses worldwide use PushEngage to send 9 billion notifications each month.',
'duplicator'
),
'https://downloads.wordpress.org/plugin/pushengage.zip',
'https://wordpress.org/plugins/pushengage/'
);
$result[$item->getSlug()] = $item;
$item = new ExtraItem(
__('Smash Balloon Instagram Feeds', 'duplicator'),
'instagram-feed/instagram-feed.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-sb-instagram.png',
__(
'Easily display Instagram content on your WordPress site without writing any code. Comes with multiple templates, ' .
'ability to show content from multiple accounts, hashtags, and more. Trusted by 1 million websites.',
'duplicator'
),
'https://downloads.wordpress.org/plugin/instagram-feed.zip',
'https://wordpress.org/plugins/instagram-feed/'
);
$item->setPro(
__('Smash Balloon Instagram Feeds Pro', 'duplicator'),
'instagram-feed-pro/instagram-feed.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-sb-instagram.png',
__(
'Easily display Instagram content on your WordPress site without writing any code. Comes with multiple ' .
'templates, ability to show content from multiple accounts, hashtags, and more. Trusted by 1 million websites.',
'duplicator'
),
'https://smashballoon.com/instagram-feed/?utm_source=duplicatorplugin&utm_medium=link&utm_campaign=About%20Duplicator'
);
$result[$item->getSlug()] = $item;
$item = new ExtraItem(
__('Smash Balloon Facebook Feeds', 'duplicator'),
'custom-facebook-feed/custom-facebook-feed.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-sb-fb.png',
__(
'Easily display Facebook content on your WordPress site without writing any code. Comes with multiple templates, ' .
'ability to embed albums, group content, reviews, live videos, comments, and reactions.',
'duplicator'
),
'https://downloads.wordpress.org/plugin/custom-facebook-feed.zip',
'https://wordpress.org/plugins/custom-facebook-feed/'
);
$item->setPro(
__('Smash Balloon Facebook Feeds Pro', 'duplicator'),
'custom-facebook-feed-pro/custom-facebook-feed.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-sb-fb.png',
__(
'Easily display Facebook content on your WordPress site without writing any code. Comes with multiple templates, ' .
'ability to embed albums, group content, reviews, live videos, comments, and reactions.',
'duplicator'
),
'https://smashballoon.com/custom-facebook-feed/?utm_source=duplicatorplugin&utm_medium=link&utm_campaign=About%20Duplicator'
);
$result[$item->getSlug()] = $item;
$item = new ExtraItem(
__('Smash Balloon Twitter Feeds', 'duplicator'),
'custom-twitter-feeds/custom-twitter-feed.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-sb-twitter.png',
__(
'Easily display Twitter content in WordPress without writing any code. Comes with multiple layouts, ability ' .
'to combine multiple Twitter feeds, Twitter card support, tweet moderation, and more.',
'duplicator'
),
'https://downloads.wordpress.org/plugin/custom-twitter-feeds.zip',
'https://wordpress.org/plugins/custom-twitter-feeds/'
);
$item->setPro(
__('Smash Balloon Twitter Feeds Pro', 'duplicator'),
'custom-twitter-feeds-pro/custom-twitter-feeds.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-sb-twitter.png',
__(
'Easily display Twitter content in WordPress without writing any code. Comes with multiple layouts, ' .
'ability to combine multiple Twitter feeds, Twitter card support, tweet moderation, and more.',
'duplicator'
),
'https://smashballoon.com/custom-twitter-feeds/?utm_source=duplicatorplugin&utm_medium=link&utm_campaign=About%20Duplicator'
);
$result[$item->getSlug()] = $item;
$item = new ExtraItem(
__('Smash Balloon YouTube Feeds', 'duplicator'),
'feeds-for-youtube/youtube-feed.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-sb-youtube.png',
__(
'Easily display YouTube videos on your WordPress site without writing any code. Comes with multiple layouts, ' .
'ability to embed live streams, video filtering, ability to combine multiple channel videos, and more.',
'duplicator'
),
'https://downloads.wordpress.org/plugin/feeds-for-youtube.zip',
'https://wordpress.org/plugins/feeds-for-youtube/'
);
$item->setPro(
__('Smash Balloon YouTube Feeds Pro', 'duplicator'),
'youtube-feed-pro/youtube-feed.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-sb-youtube.png',
__(
'Easily display YouTube videos on your WordPress site without writing any code. Comes with multiple ' .
'layouts, ability to embed live streams, video filtering, ability to combine multiple channel videos, and more.',
'duplicator'
),
'https://smashballoon.com/youtube-feed/?utm_source=duplicatorplugin&utm_medium=link&utm_campaign=About%20Duplicator'
);
$result[$item->getSlug()] = $item;
$item = new ExtraItem(
__('TrustPulse', 'duplicator'),
'trustpulse-api/trustpulse.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-trustpulse.png',
__(
'Boost your sales and conversions by up to 15% with real-time social proof notifications. TrustPulse helps ' .
'you show live user activity and purchases to help convince other users to purchase.',
'duplicator'
),
'https://downloads.wordpress.org/plugin/trustpulse-api.zip',
'https://wordpress.org/plugins/trustpulse-api/'
);
$result[$item->getSlug()] = $item;
$item = new ExtraItem(
__('SearchWP', 'duplicator'),
'searchwp/index.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-searchwp.png',
__(
'The most advanced WordPress search plugin. Customize your WordPress search algorithm, reorder search results, ' .
'track search metrics, and everything you need to leverage search to grow your business.',
'duplicator'
),
'https://searchwp.com/?utm_source=duplicatorplugin&utm_medium=link&utm_campaign=About%20Duplicator'
);
$result[$item->getSlug()] = $item;
$item = new ExtraItem(
__('AffiliateWP', 'duplicator'),
'affiliate-wp/affiliate-wp.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-affwp.png',
__(
'The #1 affiliate management plugin for WordPress. Easily create an affiliate program for your eCommerce ' .
'store or membership site within minutes and start growing your sales with the power of referral marketing.',
'duplicator'
),
'https://affiliatewp.com/?utm_source=duplicatorplugin&utm_medium=link&utm_campaign=About%20Duplicator',
false
);
$result[$item->getSlug()] = $item;
$item = new ExtraItem(
__('WP Simple Pay', 'duplicator'),
'stripe/stripe-checkout.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-wp-simple-pay.png',
__(
'The #1 Stripe payments plugin for WordPress. Start accepting one-time and recurring payments on your ' .
'WordPress site without setting up a shopping cart. No code required.',
'duplicator'
),
'https://downloads.wordpress.org/plugin/stripe.zip',
'https://wordpress.org/plugins/stripe/'
);
$item->setPro(
__('WP Simple Pay Pro', 'duplicator'),
'wp-simple-pay-pro-3/simple-pay.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-wp-simple-pay.png',
__(
'The #1 Stripe payments plugin for WordPress. Start accepting one-time and recurring payments on your ' .
'WordPress site without setting up a shopping cart. No code required.',
'duplicator'
),
'https://wpsimplepay.com/lite-upgrade/?utm_source=duplicatorplugin&utm_medium=link&utm_campaign=About%20Duplicator'
);
$result[$item->getSlug()] = $item;
$item = new ExtraItem(
__('Easy Digital Downloads', 'duplicator'),
'easy-digital-downloads/easy-digital-downloads.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-edd.png',
__('The best WordPress eCommerce plugin for selling digital downloads. Start selling eBooks, software, music, ' .
'digital art, and more within minutes. Accept payments, manage subscriptions, advanced access control, and more.', 'duplicator'),
'https://downloads.wordpress.org/plugin/easy-digital-downloads.zip',
'https://wordpress.org/plugins/easy-digital-downloads/'
);
$result[$item->getSlug()] = $item;
$item = new ExtraItem(
__('Sugar Calendar', 'duplicator'),
'sugar-calendar-lite/sugar-calendar-lite.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-sugarcalendar.png',
__('A simple & powerful event calendar plugin for WordPress that comes with all the event management features ' .
'including payments, scheduling, timezones, ticketing, recurring events, and more.', 'duplicator'),
'https://downloads.wordpress.org/plugin/sugar-calendar-lite.zip',
'https://wordpress.org/plugins/sugar-calendar-lite/'
);
$item->setPro(
__('Sugar Calendar Pro', 'duplicator'),
'sugar-calendar/sugar-calendar.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-sugarcalendar.png',
__('A simple & powerful event calendar plugin for WordPress that comes with all the event management features ' .
'including payments, scheduling, timezones, ticketing, recurring events, and more.', 'duplicator'),
'https://sugarcalendar.com/?utm_source=duplicatorplugin&utm_medium=link&utm_campaign=About%20Duplicator'
);
$result[$item->getSlug()] = $item;
$item = new ExtraItem(
__('WPCode', 'duplicator'),
'insert-headers-and-footers/ihaf.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-wpcode.png',
__('Future proof your WordPress customizations with the most popular code snippet management plugin for WordPress. ' .
'Trusted by over 1,500,000+ websites for easily adding code to WordPress right from the admin area.', 'duplicator'),
'https://downloads.wordpress.org/plugin/insert-headers-and-footers.zip',
'https://wordpress.org/plugins/insert-headers-and-footers/'
);
$result[$item->getSlug()] = $item;
$item = new ExtraItem(
__('Search & Replace Everything', 'duplicator'),
'search-replace-wpcode/wsrw.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-search-and-replace.png',
__('Efficiently manage your websites content directly from the WordPress admin with Search & Replace Everything by WPCode. ' .
'This tool is essential for site migrations, content updates, or any situation where batch text and image replacements are needed.', 'duplicator'),
'https://downloads.wordpress.org/plugin/search-replace-wpcode.zip',
'https://wordpress.org/plugins/search-replace-wpcode/'
);
$result[$item->getSlug()] = $item;
$item = new ExtraItem(
__('Uncanny Automator', 'duplicator'),
'uncanny-automator/uncanny-automator.php',
DUPLICATOR_PLUGIN_URL . 'assets/img/about/plugin-uncanny-automator.png',
__('Uncanny Automator is the easiest and most powerful way to automate your WordPress site with no code. ' .
'Build automations in minutes that connect your WordPress plugins, sites and apps together using billions of recipe combinations.', 'duplicator'),
'https://downloads.wordpress.org/plugin/uncanny-automator.zip',
'https://wordpress.org/plugins/uncanny-automator/'
);
$result[$item->getSlug()] = $item;
$item = new ExtraItem(
__('Database Reset Pro', 'duplicator'),
'db-reset-pro/db-reset-pro.php',
DUPLICATOR_PLUGIN_URL . 'assets/css/images/db-reset-icon.png',
__(
'Database Reset Pro is the safest and simplest way to reset your WordPress database to its default state.
Unlike reinstalling WordPress, this database reset plugin preserves your files, uploads,
and admin credentials while giving you a fresh start in seconds.',
'duplicator'
),
'https://downloads.wordpress.org/plugin/db-reset-pro.zip',
'https://wordpress.org/plugins/db-reset-pro/'
);
$result[$item->getSlug()] = $item;
return $result;
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace Duplicator\Utils\Help;
class Article
{
/** @var int The ID */
private $id = -1;
/** @var string The title */
private $title = '';
/** @var string Link to the article */
private $link = '';
/** @var int[] Categoriy IDs */
private $categories = [];
/** @var string[] The tags */
private $tags = [];
/**
* Constructor
*
* @param int $id The ID
* @param string $title The title
* @param string $link Link to the article
* @param int[] $categories Categories
* @param string[] $tags Tags
*/
public function __construct($id, $title, $link, $categories, $tags = array())
{
$this->id = $id;
$this->title = $title;
$this->link = $link;
$this->categories = $categories;
$this->tags = $tags;
}
/**
* Get the Title
*
* @return string
*/
public function getTitle()
{
return $this->title;
}
/**
* Get the Link
*
* @return string
*/
public function getLink()
{
return $this->link;
}
/**
* Get the Categories
*
* @return int[]
*/
public function getCategories()
{
return $this->categories;
}
/**
* Get the Tags
*
* @return string[]
*/
public function getTags()
{
return $this->tags;
}
/**
* Get the ID
*
* @return int
*/
public function getId()
{
return $this->id;
}
}

View File

@@ -0,0 +1,114 @@
<?php
namespace Duplicator\Utils\Help;
class Category
{
/** @var int The ID */
private $id = -1;
/** @var string The name */
private $name = '';
/** @var int Number of articles */
private $articleCount = 0;
/** @var Category|null The parent */
private $parent = null;
/** @var Category[] The children */
private $children = [];
/**
* Constructor
*
* @param int $id The ID
* @param string $name The name
* @param int $articleCount Number of articles
*/
public function __construct($id, $name, $articleCount)
{
$this->id = $id;
$this->name = $name;
$this->articleCount = $articleCount;
}
/**
* Get the ID
*
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* Get the Name
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Get the Article Count
*
* @return int
*/
public function getArticleCount()
{
return $this->articleCount;
}
/**
* Get the Children
*
* @return Category[]
*/
public function getChildren()
{
return $this->children;
}
/**
* Add a child
*
* @param Category $child The child
*
* @return void
*/
public function addChild(Category $child)
{
if (isset($this->children[$child->getId()])) {
return;
}
$this->children[$child->getId()] = $child;
}
/**
* Get the Parent
*
* @return Category|null
*/
public function getParent()
{
return $this->parent;
}
/**
* Set the Parent
*
* @param Category $parent The parent
*
* @return void
*/
public function setParent(Category $parent)
{
$this->parent = $parent;
}
}

View File

@@ -0,0 +1,413 @@
<?php
namespace Duplicator\Utils\Help;
use DUP_LITE_Plugin_Upgrade;
use DUP_Log;
use DUP_Settings;
use Duplicator\Controllers\HelpPageController;
use Duplicator\Libs\Snap\SnapIO;
use Duplicator\Libs\Snap\SnapJson;
use Duplicator\Core\Controllers\ControllersManager;
use Duplicator\Utils\ExpireOptions;
/*
* Dynamic Help from site documentation
*/
class Help
{
/** @var string The doc article endpoint */
const ARTICLE_ENDPOINT = 'https://www.duplicator.com/wp-json/wp/v2/ht-kb';
/** @var string The doc categories endpoint */
const CATEGORY_ENDPOINT = 'https://www.duplicator.com/wp-json/wp/v2/ht-kb-category';
/** @var string The doc tags endpoint */
const TAGS_ENDPOINT = 'https://www.duplicator.com/wp-json/wp/v2/ht-kb-tag';
/** @var int Maximum number of articles to load */
const MAX_ARTICLES = 500;
/** @var int Maximum number of categories to load */
const MAX_CATEGORY = 20;
/** @var int Maximum number of tags to load */
const MAX_TAGS = 100;
/** @var int Per page limit */
const PER_PAGE = 100;
/** @var string Cron hook */
const DOCS_EXPIRE_OPT_KEY = 'duplicator_help_docs_expire';
/** @var Article[] The articles */
private $articles = [];
/** @var Category[] The categories */
private $categories = [];
/** @var array<int, string> The tags ID => slug */
private $tags = [];
/** @var self The instance */
private static $instance = null;
/**
* Init
*
* @return void
*/
private function __construct()
{
// Update data from API if cache is expired or does not exist
if (
!ExpireOptions::getUpdate(self::DOCS_EXPIRE_OPT_KEY, true, WEEK_IN_SECONDS) ||
!$this->loadData()
) {
$this->updateData();
}
}
/**
* Get the instance
*
* @return self The instance
*/
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Get the help page URL with tag
*
* @return string The URL with tag
*/
public static function getHelpPageUrl()
{
return HelpPageController::getHelpLink() . '&tag=' . self::getCurrentPageTag();
}
/**
* Get articles by category
*
* @param int $categoryId The category ID
*
* @return Article[] The articles
*/
public function getArticlesByCategory($categoryId)
{
return array_filter($this->articles, function (Article $article) use ($categoryId) {
return in_array($categoryId, $article->getCategories());
});
}
/**
* Get articles by tag
*
* @param string $tag The tag
*
* @return Article[] The articles
*/
public function getArticlesByTag($tag)
{
if ($tag === '') {
return [];
}
return array_filter($this->articles, function (Article $article) use ($tag) {
return in_array($tag, $article->getTags());
});
}
/**
* Get top level categories.
* E.g. categories without parents & with children or articles
*
* @return Category[] The categories
*/
public function getTopLevelCategories()
{
return array_filter($this->categories, function (Category $category) {
return $category->getParent() === null && (count($category->getChildren()) > 0 || $category->getArticleCount() > 0);
});
}
/**
* Load data from API
*
* @return array{articles: mixed[], categories: mixed[], tags: mixed[]}|array<mixed> The data
*/
private function getDataFromApi()
{
$categories = $this->fetchDataFromEndpoint(
self::CATEGORY_ENDPOINT,
self::MAX_CATEGORY,
[
'id',
'name',
'count',
'parent',
]
);
$articles = $this->fetchDataFromEndpoint(
self::ARTICLE_ENDPOINT,
self::MAX_ARTICLES,
[
'id',
'title',
'link',
'ht-kb-category',
'ht-kb-tag',
]
);
$tags = $this->fetchDataFromEndpoint(
self::TAGS_ENDPOINT,
self::MAX_TAGS,
[
'id',
'slug',
]
);
if ($categories === [] || $articles === [] || $tags === []) {
DUP_Log::Trace('Failed to load from API. No data.');
return [];
}
return [
'articles' => $articles,
'categories' => $categories,
'tags' => $tags,
];
}
/**
* Load from API
*
* @param string $endpoint The endpoint
* @param int $limit Maximum number of items to load
* @param string[] $fields The fields to load
*
* @return array<mixed> The data
*/
private function fetchDataFromEndpoint($endpoint, $limit, $fields = [])
{
$result = [];
$endpointUrl = $endpoint . '?per_page=' . self::PER_PAGE;
if (count($fields) > 0) {
$endpointUrl .= '&_fields[]=' . implode('&_fields[]=', $fields);
}
$maxPages = ceil($limit / self::PER_PAGE);
for ($i = 1; $i <= $maxPages; $i++) {
$endpointUrl .= '&page=' . $i;
$response = wp_remote_get(
$endpointUrl,
['timeout' => 15]
);
if (is_wp_error($response)) {
DUP_Log::Trace("Failed to load from API: {$endpointUrl}");
DUP_Log::Trace($response->get_error_message());
return [];
}
$code = wp_remote_retrieve_response_code($response);
if ($code !== 200) {
DUP_Log::Trace("Failed to load from API: {$endpointUrl}, code: {$code}");
return [];
}
$body = wp_remote_retrieve_body($response);
if (($data = json_decode($body, true)) === null) {
DUP_Log::Trace("Failed to decode response: {$body}");
return [];
}
$result = array_merge($result, $data);
$totalPages = wp_remote_retrieve_header($response, 'x-wp-totalpages');
if ($totalPages === '' || $i >= (int) $totalPages) {
break;
}
}
$result = array_combine(array_column($result, 'id'), $result);
return $result;
}
/**
* Get the current page tag
*
* @return string The tag
*/
private static function getCurrentPageTag()
{
if (!isset($_GET['page'])) {
return '';
}
$page = $_GET['page'];
$tab = isset($_GET['tab']) ? $_GET['tab'] : '';
$innerPage = isset($_GET['inner_page']) ? $_GET['inner_page'] : '';
switch ($page) {
case ControllersManager::PACKAGES_SUBMENU_SLUG:
if ($innerPage === 'new1') {
return 'backup_step_1';
} elseif ($tab === 'new2') {
return 'backup_step_2';
} elseif ($tab === 'new3') {
return 'backup_step_3';
}
return 'backups';
case ControllersManager::IMPORT_SUBMENU_SLUG:
return 'import';
case ControllersManager::SCHEDULES_SUBMENU_SLUG:
return 'schedules';
case ControllersManager::STORAGE_SUBMENU_SLUG:
return 'storages';
case ControllersManager::TOOLS_SUBMENU_SLUG:
if ($tab === 'templates') {
return 'templates';
} elseif ($tab === 'recovery') {
return 'recovery';
}
return 'tools';
case ControllersManager::SETTINGS_SUBMENU_SLUG:
return 'settings';
default:
DUP_Log::Trace("No tag for page.");
}
return '';
}
/**
* Get the cache path
*
* @return string The cache path
*/
private static function getCacheFilePath()
{
$installInfo = DUP_LITE_Plugin_Upgrade::getInstallInfo();
return DUP_Settings::getSsdirPath() . '/cache_' . md5($installInfo['time']) . '/duplicator_help_cache.json';
}
/**
* Set from cache data
*
* @param array{articles: mixed[], categories: mixed[], tags: mixed[]} $data The data
*
* @return bool True if set
*/
private function setFromArray($data)
{
if (!isset($data['articles']) || !isset($data['categories']) || !isset($data['tags'])) {
DUP_Log::Trace("Invalid data.");
return false;
}
foreach ($data['tags'] as $tag) {
$this->tags[$tag['id']] = $tag['slug'];
}
foreach ($data['categories'] as $category) {
$this->categories[$category['id']] = new Category(
$category['id'],
$category['name'],
$category['count']
);
}
foreach ($this->categories as $category) {
if (
($parentId = $data['categories'][$category->getId()]['parent']) === 0 ||
!isset($this->categories[$parentId])
) {
continue;
}
$this->categories[$parentId]->addChild($category);
$category->setParent($this->categories[$parentId]);
}
foreach ($data['articles'] as $article) {
$this->articles[$article['id']] = new Article(
$article['id'],
$article['title']['rendered'],
$article['link'],
$article['ht-kb-category'],
array_map(function ($tagId) {
return $this->tags[$tagId];
}, $article['ht-kb-tag'])
);
}
return true;
}
/**
* Get data from cache
*
* @return bool True if loaded
*/
private function loadData()
{
if (!file_exists(self::getCacheFilePath())) {
DUP_Log::Trace("Cache file does not exist: " . self::getCacheFilePath());
return false;
}
if (($contents = file_get_contents(self::getCacheFilePath())) === false) {
DUP_Log::Trace("Failed to read cache file: " . self::getCacheFilePath());
return false;
}
if (($data = json_decode($contents, true)) === null) {
DUP_Log::Trace("Failed to decode cache file: " . self::getCacheFilePath());
return false;
}
return $this->setFromArray($data);
}
/**
* Save to cache
*
* @return bool True if saved
*/
public function updateData()
{
if (($data = $this->getDataFromApi()) === []) {
DUP_Log::Trace("Failed to load data from API.");
return false;
}
$cachePath = self::getCacheFilePath();
$cacheDir = dirname($cachePath);
if (!file_exists($cacheDir) && !SnapIO::mkdir($cacheDir, 0755, true)) {
DUP_Log::Trace("Failed to create cache directory: {$cacheDir}");
return false;
}
if (($encoded = SnapJson::jsonEncode($data)) === false) {
DUP_Log::Trace("Failed to encode cache data.");
return false;
}
if (file_put_contents(self::getCacheFilePath(), $encoded) === false) {
DUP_Log::Trace("Failed to write cache file: {$cachePath}");
return false;
}
return $this->setFromArray($data);
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* @package Duplicator
*/
namespace Duplicator\Utils;
use Duplicator\Installer\Utils\InstallerLinkManager;
/**
* Link manager class
*/
class LinkManager extends InstallerLinkManager
{
/**
* @param string|string[] $paths The path to the article
* @param string $medium The utm medium
* @param string $content The utm content
*
* @return string The url with path and utm params
*/
protected static function buildUrl($paths, $medium, $content)
{
return apply_filters('duplicator_upsell_url_filter', parent::buildUrl($paths, $medium, $content));
}
}

View File

@@ -0,0 +1,141 @@
<?php
namespace Duplicator\Utils\Support;
use DUP_Log;
use DUP_Package;
use DUP_Server;
use DUP_Settings;
use Duplicator\Libs\Snap\SnapIO;
use Duplicator\Libs\Snap\SnapUtil;
use Duplicator\Utils\ZipArchiveExtended;
use Exception;
class SupportToolkit
{
const SUPPORT_TOOLKIT_BACKUP_NUMBER = 10;
const SUPPORT_TOOLKIT_PREFIX = 'duplicator_support_toolkit_';
/**
* Returns true if the diagnostic data can be downloaded
*
* @return bool true if diagnostic info can be downloaded
*/
public static function isAvailable()
{
return ZipArchiveExtended::isPhpZipAvailable();
}
/**
* Returns the diagnostic data download URL if available,
* empty string otherwise.
*
* @return string
*/
public static function getSupportToolkitDownloadUrl()
{
if (!self::isAvailable()) {
return '';
}
return admin_url('admin-ajax.php') . '?' . http_build_query([
'action' => 'duplicator_download_support_toolkit',
'nonce' => wp_create_nonce('duplicator_download_support_toolkit'),
]);
}
/**
* Generates a support toolkit zip file
*
* @return string The path to the generated zip file
*/
public static function getToolkit()
{
$tempZipFilePath = DUP_Settings::getSsdirTmpPath() . '/' .
self::SUPPORT_TOOLKIT_PREFIX . date('YmdHis') . '_' .
SnapUtil::generatePassword(16, false, false) . '.zip';
$zip = new ZipArchiveExtended($tempZipFilePath);
if ($zip->open() === false) {
throw new Exception(__('Failed to create zip file', 'duplicator'));
}
// Trace log
if (DUP_Settings::Get('trace_log_enabled')) {
$zip->addFile(DUP_Log::getTraceFilepath());
}
// Debug log (if it exists)
if (WP_DEBUG_LOG !== false) {
if (is_bool(WP_DEBUG_LOG) && WP_DEBUG_LOG === true) {
$zip->addFile(
trailingslashit(wp_normalize_path(realpath(WP_CONTENT_DIR))) . 'debug.log',
'',
10 * MB_IN_BYTES
);
} elseif (is_string(WP_DEBUG_LOG) && strlen(WP_DEBUG_LOG) > 0) {
//The path can be relative too so resolve via safepath
$zip->addFile(
SnapIO::safePath(WP_DEBUG_LOG, true),
'',
10 * MB_IN_BYTES
);
}
}
//phpinfo (as html)
$zip->addFileFromString('phpinfo.html', self::getPhpInfo());
//custom server settings info (as html)
$zip->addFileFromString('serverinfo.txt', self::getPlainServerSettings());
//Last 10 backup build logs
DUP_Package::by_status_callback(
function (DUP_Package $package) use ($zip) {
$file_path = DUP_Settings::getSsdirLogsPath() . "/" . $package->getLogFilename();
$zip->addFile($file_path);
},
[],
self::SUPPORT_TOOLKIT_BACKUP_NUMBER,
0,
'`id` DESC'
);
return $tempZipFilePath;
}
/**
* Returns the contents of the "Server Settings" section in "Tools" > "General" in plain text format
*
* @return string
*/
private static function getPlainServerSettings()
{
$result = '';
foreach (DUP_Server::getServerSettingsData() as $section) {
$result .= $section['title'] . "\n";
$result .= str_repeat('=', 50) . "\n";
foreach ($section['settings'] as $data) {
$result .= str_pad($data['logLabel'], 20, ' ', STR_PAD_RIGHT) . ' ' . $data['value'] . "\n";
}
$result .= "\n\n";
}
return $result;
}
/**
* Returns the output of phpinfo as a string
*
* @return string
*/
private static function getPhpInfo()
{
ob_start();
SnapUtil::phpinfo();
$phpInfo = ob_get_clean();
return $phpInfo === false ? '' : $phpInfo;
}
}

View File

@@ -0,0 +1,69 @@
<?php
/**
* @package Duplicator
*/
namespace Duplicator\Utils;
/**
* Upsell class, this class is used on plugin and installer
*/
class Upsell
{
/**
* Get Pro features list
*
* @return string[]
*/
public static function getProFeatureList()
{
return array(
__('Scheduled Backups', 'duplicator'),
__('Recovery Points', 'duplicator'),
__('Secure File Encryption', 'duplicator'),
__('Server to Server Import', 'duplicator'),
__('File & Database Table Filters', 'duplicator'),
__('Cloud Storage - Google Drive', 'duplicator'),
__('Cloud Storage - Amazon S3', 'duplicator'),
__('Cloud Storage - DropBox', 'duplicator'),
__('Cloud Storage - OneDrive', 'duplicator'),
__('Cloud Storage - FTP/SFTP', 'duplicator'),
__('Drag & Drop Installs', 'duplicator'),
__('Larger Site Support', 'duplicator'),
__('Multisite Network Support', 'duplicator'),
__('Email Alerts', 'duplicator'),
__('Advanced Backup Permissions', 'duplicator')
);
}
/**
* Get Pro callout features list
*
* @return string[]
*/
public static function getCalloutCTAFeatureList()
{
return array(
__('Scheduled Backups', 'duplicator'),
__('Recovery Points', 'duplicator'),
__('Secure File Encryption', 'duplicator'),
__('Server to Server Import', 'duplicator'),
__('File & Database Table Filters', 'duplicator'),
__('Cloud Storage', 'duplicator'),
__('Smart Migration Wizard', 'duplicator'),
__('Drag & Drop Installs', 'duplicator'),
__('Streamlined Installer', 'duplicator'),
__('Developer Hooks', 'duplicator'),
__('Managed Hosting Support', 'duplicator'),
__('Larger Site Support', 'duplicator'),
__('Installer Branding', 'duplicator'),
__('Migrate Duplicator Settings', 'duplicator'),
__('Regenerate SALTS', 'duplicator'),
__('Multisite Network', 'duplicator'),
__('Email Alerts', 'duplicator'),
__('Custom Search & Replace', 'duplicator'),
__('Advanced Backup Permissions', 'duplicator')
);
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace Duplicator\Utils\UsageStatistics;
use DUP_Log;
use Duplicator\Libs\Snap\SnapLog;
use Error;
use Exception;
use WP_Error;
class CommStats
{
const API_VERSION = '1.0';
const DEFAULT_REMOTE_HOST = 'https://connect.duplicator.com';
const END_POINT_PLUGIN_STATS = '/api/ustats/addLiteStats';
const END_POINT_DISABLE = '/api/ustats/disable';
const END_POINT_INSTALLER = '/api/ustats/installer';
/**
* Send plugin statistics
*
* @return bool true if data was sent successfully, false otherwise
*/
public static function pluginSend()
{
if (!StatsBootstrap::isTrackingAllowed()) {
return false;
}
$data = PluginData::getInstance()->getDataToSend();
if (self::request(self::END_POINT_PLUGIN_STATS, $data)) {
PluginData::getInstance()->updateLastSendTime();
return true;
} else {
return false;
}
}
/**
* Disabled usage tracking
*
* @return bool true if data was sent successfully, false otherwise
*/
public static function disableUsageTracking()
{
if (DUPLICATOR_USTATS_DISALLOW) { // @phpstan-ignore-line
// Don't use StatsBootstrap::isTrackingAllowed beacause on disalbe usage tracking i necessary disable the tracking on server
return false;
}
// Remove usage tracking data on server
$data = PluginData::getInstance()->getDisableDataToSend();
return self::request(self::END_POINT_DISABLE, $data, 'Disable usage tracking error');
}
/**
* Sent installer statistics
*
* @return bool true if data was sent successfully, false otherwise
*/
public static function installerSend()
{
if (!StatsBootstrap::isTrackingAllowed()) {
return false;
}
$data = InstallerData::getInstance()->getDataToSend();
return self::request(self::END_POINT_INSTALLER, $data, 'Installer usage tracking error');
}
/**
* Request to usage tracking server
*
* @param string $endPoint end point
* @param array<string, mixed> $data data to send
* @param string $traceMessagePerefix trace message prefix
*
* @return bool true if data was sent successfully, false otherwise
*/
protected static function request($endPoint, $data, $traceMessagePerefix = 'Error sending usage tracking')
{
try {
global $wp_version;
$agent_string = "WordPress/" . $wp_version;
$postParams = array(
'method' => 'POST',
'timeout' => 10,
'redirection' => 5,
'sslverify' => false,
'httpversion' => '1.1',
//'blocking' => false,
'user-agent' => $agent_string,
'body' => $data,
);
$url = self::getRemoteHost() . $endPoint . '/';
$response = wp_remote_post($url, $postParams);
if (is_wp_error($response)) {
/** @var WP_Error $response */
DUP_Log::trace('URL Request: ' . $url);
DUP_Log::trace($traceMessagePerefix . ' code: ' . $response->get_error_code());
DUP_Log::trace('Error message: ' . $response->get_error_message());
return false;
} elseif ($response['response']['code'] < 200 || $response['response']['code'] >= 300) {
DUP_Log::trace('URL Request: ' . $url);
DUP_Log::trace($traceMessagePerefix . ' code: ' . $response['response']['code']);
DUP_Log::trace('Error message: ' . $response['response']['message']);
DUP_Log::traceObject('Data', $data);
return false;
} else {
DUP_Log::trace('Usage tracking updated successfully');
return true;
}
} catch (Exception $e) {
DUP_Log::trace($traceMessagePerefix . ' trace msg: ' . $e->getMessage() . "\n" . SnapLog::getTextException($e, false));
return false;
} catch (Error $e) {
DUP_Log::trace($traceMessagePerefix . ' trace msg: ' . $e->getMessage() . "\n" . SnapLog::getTextException($e, false));
return false;
}
}
/**
* Get remote host
*
* @return string
*/
public static function getRemoteHost()
{
if (DUPLICATOR_CUSTOM_STATS_REMOTE_HOST != '') { // @phpstan-ignore-line
return DUPLICATOR_CUSTOM_STATS_REMOTE_HOST;
} else {
return self::DEFAULT_REMOTE_HOST;
}
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Duplicator\Utils\UsageStatistics;
use DUP_DB;
use Duplicator\Core\MigrationMng;
use Duplicator\Libs\Snap\SnapDB;
use Duplicator\Libs\Snap\SnapUtil;
use wpdb;
class InstallerData
{
/**
* @var ?self
*/
private static $instance = null;
/**
* Class constructor
*/
private function __construct()
{
}
/**
* Get instance
*
* @return self
*/
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Return usage tracking data
*
* @return array<string, mixed>
*/
public function getDataToSend()
{
/** @var wpdb $wpdb */
global $wpdb;
$data = (object) MigrationMng::getMigrationData();
$result = array(
'api_version' => CommStats::API_VERSION,
'plugin' => $data->plugin,
'plugin_version' => $data->installerVersion,
'install_type' => StatsUtil::getInstallType($data->installType),
'logic_modes' => StatsUtil::getLogicModes($data->logicModes),
'template' => StatsUtil::getTemplate($data->template),
'wp_version' => get_bloginfo('version'),
'db_engine' => SnapDB::getDBEngine($wpdb->dbh), // @phpstan-ignore-line
'db_version' => DUP_DB::getVersion(),
// SOURCE SITE INFO
'source_phpv' => SnapUtil::getVersion($data->phpVersion, 3),
// TARGET SITE INFO
'target_phpv' => SnapUtil::getVersion(phpversion(), 3),
// PACKAGE INFO
'license_type' => StatsUtil::getLicenseType($data->licenseType),
'archive_type' => $data->archiveType,
'site_size_mb' => round(((int) $data->siteSize) / 1024 / 1024, 2),
'site_num_files' => $data->siteNumFiles,
'site_db_size_mb' => round(((int) $data->siteDbSize) / 1024 / 1024, 2),
'site_db_num_tbl' => $data->siteDBNumTables,
'components' => StatsUtil::getStatsComponents($data->components),
);
$rules = array(
'api_version' => 'string|max:7', // 1.0
'plugin_version' => 'string|max:25',
'wp_version' => 'string|max:25',
'db_engine' => 'string|max:25',
'db_version' => 'string|max:25',
// SOURCE SERVER INFO
'source_phpv' => 'string|max:25',
// TARGET SERVER INFO
'target_phpv' => 'string|max:25',
);
return StatsUtil::sanitizeFields($result, $rules);
}
}

View File

@@ -0,0 +1,434 @@
<?php
namespace Duplicator\Utils\UsageStatistics;
use DUP_DB;
use DUP_LITE_Plugin_Upgrade;
use DUP_Log;
use DUP_Package;
use DUP_PackageStatus;
use Duplicator\Libs\Snap\SnapDB;
use Duplicator\Libs\Snap\SnapJson;
use Duplicator\Libs\Snap\SnapUtil;
use Duplicator\Libs\Snap\SnapWP;
use ReflectionClass;
use stdClass;
use wpdb;
class PluginData
{
const PLUGIN_DATA_OPTION_KEY = 'duplicator_plugin_data_stats';
const IDENTIFIER_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.,;=+&';
const PLUGIN_STATUS_ACTIVE = 'active';
const PLUGIN_STATUS_INACTIVE = 'inactive';
/**
* @var ?self
*/
private static $instance = null;
/**
* @var int
*/
private $lastSendTime = 0;
/**
* @var string
*/
private $identifier = '';
/**
* @var string
*/
private $plugin = 'dup-lite';
/**
* @var string
*/
private $pluginStatus = self::PLUGIN_STATUS_ACTIVE;
/**
* @var int
*/
private $buildCount = 0;
/**
* @var int
*/
private $buildLastDate = 0;
/**
* @var int
*/
private $buildFailedCount = 0;
/**
* @var int
*/
private $buildFailedLastDate = 0;
/**
* @var float
*/
private $siteSizeMB = 0;
/**
* @var int
*/
private $siteNumFiles = 0;
/**
* @var float
*/
private $siteDbSizeMB = 0;
/**
* @var int
*/
private $siteDbNumTables = 0;
/**
* Class constructor
*/
private function __construct()
{
if (($data = get_option(self::PLUGIN_DATA_OPTION_KEY)) !== false) {
$data = json_decode($data, true);
$reflect = new ReflectionClass(__CLASS__);
$props = $reflect->getProperties();
foreach ($props as $prop) {
if (isset($data[$prop->getName()])) {
$prop->setAccessible(true);
$prop->setValue($this, $data[$prop->getName()]);
}
}
} else {
$this->identifier = self::generateIdentifier();
$this->save();
}
}
/**
* Get instance
*
* @return self
*/
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Save plugin data
*
* @return bool True if data has been saved, false otherwise
*/
public function save()
{
$values = get_object_vars($this);
return update_option(self::PLUGIN_DATA_OPTION_KEY, SnapJson::jsonEncodePPrint($values));
}
/**
* Get identifier
*
* @return string
*/
public function getIdentifier()
{
return $this->identifier;
}
/**
* Update from migrate data
*
* @param StdClass $data Migration data
*
* @return bool
*/
public function updateFromMigrateData(stdClass $data)
{
$save = false;
if (
isset($data->ustatIdentifier) &&
strlen($data->ustatIdentifier) > 0 &&
$data->ustatIdentifier !== $this->identifier
) {
$this->identifier = $data->ustatIdentifier;
$save = true;
}
return ($save ? $this->save() : true);
}
/**
* Return usage tracking data
*
* @return array<string, mixed>
*/
public function getDataToSend()
{
$result = $this->getBasicInfos();
$result = array_merge($result, $this->getPluginInfos());
$result = array_merge($result, $this->getSiteInfos());
$result = array_merge($result, $this->getManualPackageInfos());
$result = array_merge($result, $this->getSettingsInfos());
$rules = array(
'api_version' => 'string|max:7', // 1.0
'identifier' => 'string|max:44',
// BASIC INFO
'plugin_version' => 'string|max:25',
'php_version' => 'string|max:25',
'wp_version' => 'string|max:25',
// PLUGIN INFO
'pinstall_version' => '?string|max:25',
// SITE INFO
'servertype' => 'string|max:25',
'db_engine' => 'string|max:25',
'db_version' => 'string|max:25',
'timezoneoffset' => 'string|max:10',
'locale' => 'string|max:10',
'themename' => 'string|max:255',
'themeversion' => 'string|max:25',
);
return StatsUtil::sanitizeFields($result, $rules);
}
/**
* Get disable tracking data
*
* @return array<string, mixed>
*/
public function getDisableDataToSend()
{
$result = $this->getBasicInfos();
$rules = array(
'api_version' => 'string|max:7', // 1.0
'identifier' => 'string|max:44',
// BASIC INFO
'plugin_version' => 'string|max:25',
'php_version' => 'string|max:25',
'wp_version' => 'string|max:25',
);
return StatsUtil::sanitizeFields($result, $rules);
}
/**
* Set status
*
* @param string $status Status: active, inactive or uninstalled
*
* @return void
*/
public function setStatus($status)
{
if ($this->pluginStatus === $status) {
return;
}
switch ($status) {
case self::PLUGIN_STATUS_ACTIVE:
case self::PLUGIN_STATUS_INACTIVE:
$this->pluginStatus = $status;
$this->save();
break;
}
}
/**
* Get status
*
* @return string Enum: self::PLUGIN_STATUS_ACTIVE, self::PLUGIN_STATUS_INACTIVE or self::PLUGIN_STATUS_UNINSTALLED
*/
public function getStatus()
{
return $this->pluginStatus;
}
/**
* Add paackage build count and date for manual and schedule build
*
* @param DUP_Package $package Package
*
* @return void
*/
public function addPackageBuild(DUP_Package $package)
{
if ($package->Status == DUP_PackageStatus::COMPLETE) {
$this->buildCount++;
$this->buildLastDate = time();
} else {
$this->buildFailedCount++;
$this->buildFailedLastDate = time();
}
$this->save();
}
/**
* Set site size
*
* @param int $size Site size in bytes
* @param int $numFiles Number of files
* @param int $dbSize Database size in bytes
* @param int $numTables Number of tables
*
* @return void
*/
public function setSiteSize($size, $numFiles, $dbSize, $numTables)
{
$this->siteSizeMB = round(((int) $size) / 1024 / 1024, 2);
$this->siteNumFiles = (int) $numFiles;
$this->siteDbSizeMB = round(((int) $dbSize) / 1024 / 1024, 2);
$this->siteDbNumTables = (int) $numTables;
$this->save();
}
/**
* Update last send time
*
* @return void
*/
public function updateLastSendTime()
{
$this->lastSendTime = time();
$this->save();
}
/**
* Get last send time
*
* @return int
*/
public function getLastSendTime()
{
return $this->lastSendTime;
}
/**
* Get basic infos
*
* @return array<string, mixed>
*/
protected function getBasicInfos()
{
return array(
'api_version' => CommStats::API_VERSION,
'identifier' => $this->identifier,
'plugin' => $this->plugin,
'plugin_status' => $this->pluginStatus,
'plugin_version' => DUPLICATOR_VERSION,
'php_version' => SnapUtil::getVersion(phpversion(), 3),
'wp_version' => get_bloginfo('version'),
);
}
/**
* Return plugin infos
*
* @return array<string, mixed>
*/
protected function getPluginInfos()
{
if (($installInfo = DUP_LITE_Plugin_Upgrade::getInstallInfo()) === false) {
$installInfo = array(
'version' => null,
'time' => null,
);
}
return array(
'pinstall_date' => ($installInfo['time'] == null ? null : date('Y-m-d H:i:s', $installInfo['time'])),
'pinstall_version' => ($installInfo['version'] == null ? null : $installInfo['version']),
'license_type' => StatsUtil::getLicenseType(),
'license_status' => StatsUtil::getLicenseStatus(),
);
}
/**
* Return site infos
*
* @return array<string, mixed>
*/
protected function getSiteInfos()
{
/** @var wpdb $wpdb */
global $wpdb;
$theme_data = wp_get_theme();
return array(
'servertype' => StatsUtil::getServerType(),
'db_engine' => SnapDB::getDBEngine($wpdb->dbh), // @phpstan-ignore-line
'db_version' => DUP_DB::getVersion(),
'is_multisite' => is_multisite(),
'sites_count' => count(SnapWP::getSitesIds()),
'user_count' => SnapWp::getUsersCount(),
'timezoneoffset' => get_option('gmt_offset'), /** @todo evaluate use wp or server timezone offset */
'locale' => get_locale(),
'am_family' => StatsUtil::getAmFamily(),
'themename' => $theme_data->get('Name'),
'themeversion' => $theme_data->get('Version'),
'site_size_mb' => ($this->siteSizeMB == 0 ? null : $this->siteSizeMB),
'site_num_files' => ($this->siteNumFiles == 0 ? null : $this->siteNumFiles),
'site_db_size_mb' => ($this->siteDbSizeMB == 0 ? null : $this->siteDbSizeMB),
'site_db_num_tbl' => ($this->siteDbNumTables == 0 ? null : $this->siteDbNumTables),
);
}
/**
* Return manal package infos
*
* @return array<string, mixed>
*/
protected function getManualPackageInfos()
{
return array(
'packages_build_count' => $this->buildCount,
'packages_build_last_date' => ($this->buildLastDate == 0 ? null : date('Y-m-d H:i:s', $this->buildLastDate)),
'packages_build_failed_count' => $this->buildFailedCount,
'packages_build_failed_last_date' => ($this->buildFailedLastDate == 0 ? null : date('Y-m-d H:i:s', $this->buildFailedLastDate)),
'packages_count' => DUP_Package::getNumCompletePackages(),
);
}
/**
* Return granular permissions infos
*
* @return array<string, mixed>
*/
protected function getSettingsInfos()
{
return array(
'settings_archive_build_mode' => StatsUtil::getArchiveBuildMode(),
'settings_db_build_mode' => StatsUtil::getDbBuildMode(),
'settings_usage_enabled' => StatsBootstrap::isTrackingAllowed(),
);
}
/**
* Return unique identifier
*
* @return string
*/
protected static function generateIdentifier()
{
$maxRand = strlen(self::IDENTIFIER_CHARS) - 1;
$result = '';
for ($i = 0; $i < 44; $i++) {
$result .= substr(self::IDENTIFIER_CHARS, wp_rand(0, $maxRand), 1);
}
return $result;
}
}

View File

@@ -0,0 +1,136 @@
<?php
namespace Duplicator\Utils\UsageStatistics;
use DUP_Log;
use DUP_Package;
use DUP_PackageStatus;
use DUP_Settings;
use Duplicator\Utils\CronUtils;
/**
* StatsBootstrap
*/
class StatsBootstrap
{
const USAGE_TRACKING_CRON_HOOK = 'duplicator_usage_tracking_cron';
/**
* Init WordPress hooks
*
* @return void
*/
public static function init()
{
add_action('duplicator_after_activation', array(__CLASS__, 'activationAction'));
add_action('duplicator_after_deactivation', array(__CLASS__, 'deactivationAction'));
add_action('duplicator_package_after_set_status', array(__CLASS__, 'addPackageBuild'), 10, 2);
add_action('duplicator_after_scan_report', array(__CLASS__, 'addSiteSizes'), 10, 2);
add_action('duplicator_usage_tracking_cron', array(__CLASS__, 'sendPluginStatCron'));
}
/**
* Activation action
*
* @return void
*/
public static function activationAction()
{
// Set cron
if (!wp_next_scheduled(self::USAGE_TRACKING_CRON_HOOK)) {
$randomTracking = wp_rand(0, WEEK_IN_SECONDS);
$timeToStart = strtotime('next sunday') + $randomTracking;
wp_schedule_event($timeToStart, CronUtils::INTERVAL_WEEKLY, self::USAGE_TRACKING_CRON_HOOK);
}
if (PluginData::getInstance()->getStatus() !== PluginData::PLUGIN_STATUS_ACTIVE) {
PluginData::getInstance()->setStatus(PluginData::PLUGIN_STATUS_ACTIVE);
CommStats::pluginSend();
}
}
/**
* Deactivation action
*
* @return void
*/
public static function deactivationAction()
{
// Unschedule custom cron event for cleanup if it's scheduled
if (wp_next_scheduled(self::USAGE_TRACKING_CRON_HOOK)) {
$timestamp = wp_next_scheduled(self::USAGE_TRACKING_CRON_HOOK);
wp_unschedule_event($timestamp, self::USAGE_TRACKING_CRON_HOOK);
}
PluginData::getInstance()->setStatus(PluginData::PLUGIN_STATUS_INACTIVE);
CommStats::pluginSend();
}
/**
* Add package build,
* don't use PluginData::getInstance()->addPackageBuild() directly in hook to avoid useless init
*
* @param DUP_Package $package Package
* @param int $status Status DUP_PRO_PackageStatus Enum
*
* @return void
*/
public static function addPackageBuild(DUP_Package $package, $status)
{
if ($status >= DUP_PackageStatus::CREATED && $status < DUP_PackageStatus::COMPLETE) {
return;
}
PluginData::getInstance()->addPackageBuild($package);
}
/**
* Add site size statistics
*
* @param DUP_Package $package Package
* @param array<string, mixed> $report Scan report
*
* @return void
*/
public static function addSiteSizes(DUP_Package $package, $report)
{
if ($package->Archive->ExportOnlyDB) {
return;
}
PluginData::getInstance()->setSiteSize(
$report['ARC']['USize'],
$report['ARC']['UFullCount'],
$report['DB']['RawSize'],
$report['DB']['TableCount']
);
}
/**
* Is tracking allowed
*
* @return bool
*/
public static function isTrackingAllowed()
{
if (DUPLICATOR_USTATS_DISALLOW) { // @phpstan-ignore-line
return false;
}
return DUP_Settings::Get('usage_tracking', false);
}
/**
* Send plugin statistics
*
* @return void
*/
public static function sendPluginStatCron()
{
if (!self::isTrackingAllowed()) {
return;
}
DUP_Log::trace("CRON: Sending plugin statistics");
CommStats::pluginSend();
}
}

View File

@@ -0,0 +1,261 @@
<?php
namespace Duplicator\Utils\UsageStatistics;
use DUP_Archive_Build_Mode;
use DUP_DB;
use DUP_Settings;
use Duplicator\Libs\Snap\SnapUtil;
use Duplicator\Libs\Snap\SnapWP;
use Exception;
class StatsUtil
{
/**
* Get server type
*
* @return string
*/
public static function getServerType()
{
if (empty($_SERVER['SERVER_SOFTWARE'])) {
return 'unknown';
}
return SnapUtil::sanitizeNSCharsNewlineTrim(wp_unslash($_SERVER['SERVER_SOFTWARE']));
}
/**
* Get db mode
*
* @return string
*/
public static function getDbBuildMode()
{
switch (DUP_DB::getBuildMode()) {
case DUP_DB::BUILD_MODE_MYSQLDUMP:
return 'mysqldump';
case DUP_DB::BUILD_MODE_PHP_SINGLE_THREAD:
return 'php-single';
default:
throw new Exception('Unknown db build mode');
}
}
/**
* Get archive mode
*
* @return string
*/
public static function getArchiveBuildMode()
{
if (DUP_Settings::Get('archive_build_mode') == DUP_Archive_Build_Mode::ZipArchive) {
return 'zip-single';
} else {
return 'dup';
}
}
/**
* Return license types
*
* @param ?int $type License type, if null will use current license type
*
* @return string
*/
public static function getLicenseType($type = null)
{
return 'unlicensed';
}
/**
* Return license status
*
* @return string
*/
public static function getLicenseStatus()
{
return 'invalid';
}
/**
* Get install type
*
* @param int $type Install type
*
* @return string
*/
public static function getInstallType($type)
{
switch ($type) {
case -1:
return 'single';
case 4:
return 'single_on_subdomain';
case 5:
return 'single_on_subfolder';
case 8:
return 'rbackup_single';
default:
return 'not_set';
}
}
/**
* Get stats components
*
* @param string[] $components Components
*
* @return string
*/
public static function getStatsComponents($components)
{
$result = array();
foreach ($components as $component) {
switch ($component) {
case 'package_component_db':
$result[] = 'db';
break;
case 'package_component_core':
$result[] = 'core';
break;
case 'package_component_plugins':
$result[] = 'plugins';
break;
case 'package_component_plugins_active':
$result[] = 'plugins_active';
break;
case 'package_component_themes':
$result[] = 'themes';
break;
case 'package_component_themes_active':
$result[] = 'themes_active';
break;
case 'package_component_uploads':
$result[] = 'uploads';
break;
case 'package_component_other':
$result[] = 'other';
break;
}
}
return implode(',', $result);
}
/**
* Get am family plugins
*
* @return string
*/
public static function getAmFamily()
{
$result = array();
$result[] = 'dup-pro';
if (SnapWP::isPluginInstalled('duplicator/duplicator.php')) {
$result[] = 'dup-lite';
}
return implode(',', $result);
}
/**
* Get logic modes
*
* @param string[] $modes Logic modes
*
* @return string
*/
public static function getLogicModes($modes)
{
$result = array();
foreach ($modes as $mode) {
switch ($mode) {
case 'CLASSIC':
$result[] = 'CLASSIC';
break;
case 'OVERWRITE':
$result[] = 'OVERWRITE';
break;
case 'RESTORE_BACKUP':
$result[] = 'RESTORE';
break;
}
}
return implode(',', $result);
}
/**
* Get template
*
* @param string $template Template
*
* @return string
*/
public static function getTemplate($template)
{
switch ($template) {
case 'base':
return 'CLASSIC_BASE';
case 'import-base':
return 'IMPORT_BASE';
case 'import-advanced':
return 'IMPORT_ADV';
case 'recovery':
return 'RECOVERY';
case 'default':
default:
return 'CLASSIC_ADV';
}
}
/**
* Sanitize fields with rule string
* [nullable][type][|max:number]
* - ?string|max:25
* - int
*
* @param array<string, mixed> $data Data
* @param array<string, string> $rules Rules
*
* @return array<string, mixed>
*/
public static function sanitizeFields($data, $rules)
{
foreach ($data as $key => $val) {
if (!isset($rules[$key])) {
continue;
}
$matches = null;
if (preg_match('/(\??)(int|float|bool|string)(?:\|max:(\d+))?/', $rules[$key], $matches) !== 1) {
throw new Exception("Invalid sanitize rule: {$rules[$key]}");
}
$nullable = $matches[1] === '?';
$type = $matches[2];
$max = isset($matches[3]) ? (int) $matches[3] : PHP_INT_MAX;
if ($nullable && $val === null) {
continue;
}
switch ($type) {
case 'int':
$data[$key] = (int) $val;
break;
case 'float':
$data[$key] = (float) $val;
break;
case 'bool':
$data[$key] = (bool) $val;
break;
case 'string':
$data[$key] = substr((string) $val, 0, $max);
break;
default:
throw new Exception("Unknown sanitize rule: {$rules[$key]}");
}
}
return $data;
}
}

View File

@@ -0,0 +1,383 @@
<?php
/**
* @package Duplicator
*/
namespace Duplicator\Utils;
use Duplicator\Libs\Snap\SnapIO;
use Duplicator\Libs\Snap\SnapLog;
use Duplicator\Libs\Snap\SnapUtil;
use Exception;
use ZipArchive;
class ZipArchiveExtended
{
/** @var string */
protected $archivePath = '';
/** @var ZipArchive */
protected $zipArchive = null;
/** @var bool */
protected $isOpened = false;
/** @var bool */
protected $compressed = false;
/** @var bool */
protected $encrypt = false;
/** @var string */
protected $password = '';
/**
* Class constructor
*
* @param string $path zip archive path
*/
public function __construct($path)
{
if (!self::isPhpZipAvailable()) {
throw new Exception('ZipArchive PHP module is not installed/enabled.');
}
if (file_exists($path) && (!is_file($path) || !is_writeable($path))) {
throw new Exception('File ' . SnapLog::v2str($path) . 'exists but isn\'t valid');
}
$this->archivePath = $path;
$this->zipArchive = new ZipArchive();
$this->setCompressed(true);
}
/**
* Class destructor
*/
public function __destruct()
{
$this->close();
}
/**
* Check if class ZipArchvie is available
*
* @return bool
*/
public static function isPhpZipAvailable()
{
return SnapUtil::classExists(ZipArchive::class);
}
/**
* Add full dir in archive
*
* @param string $dirPath dir path
* @param string $archivePath local archive path
*
* @return bool TRUE on success or FALSE on failure.
*/
public function addDir($dirPath, $archivePath)
{
if (!is_dir($dirPath) || !is_readable($dirPath)) {
return false;
}
$dirPath = SnapIO::safePathTrailingslashit($dirPath);
$archivePath = SnapIO::safePathTrailingslashit($archivePath);
$thisObj = $this;
return SnapIO::regexGlobCallback(
$dirPath,
function ($path) use ($dirPath, $archivePath, $thisObj) {
$newPath = $archivePath . SnapIO::getRelativePath($path, $dirPath);
if (is_dir($path)) {
$thisObj->addEmptyDir($newPath);
} else {
$thisObj->addFile($path, $newPath);
}
},
array('recursive' => true)
);
}
/**
* Add empty dir on zip archive
*
* @param string $path archive dir to add
*
* @return bool TRUE on success or FALSE on failure.
*/
public function addEmptyDir($path)
{
return $this->zipArchive->addEmptyDir($path);
}
/**
* Add file on zip archive
*
* @param string $filepath file path
* @param string $archivePath archive path, if empty use file name
* @param int $maxSize max size of file, if the size is grater than this value the file will be truncated, 0 for no limit
*
* @return bool TRUE on success or FALSE on failure.
*/
public function addFile($filepath, $archivePath = '', $maxSize = 0)
{
if (!is_file($filepath) || !is_readable($filepath)) {
return false;
}
if (strlen($archivePath) === 0) {
$archivePath = basename($filepath);
}
if ($maxSize > 0 && filesize($filepath) > $maxSize) {
if (($content = file_get_contents($filepath, false, null, 0, $maxSize)) === false) {
return false;
}
$result = $this->zipArchive->addFromString($archivePath, $content);
} else {
$result = $this->zipArchive->addFile($filepath, $archivePath);
}
if ($result && $this->encrypt) {
$this->zipArchive->setEncryptionName($archivePath, ZipArchive::EM_AES_256);
}
if ($result && !$this->compressed) {
$this->zipArchive->setCompressionName($archivePath, ZipArchive::CM_STORE);
}
return $result;
}
/**
* Creates a temporary file with the given content. The file will be deleted after the zip is created
*
* @param string $archivePath Filename in archive
* @param string $content Content of the file
* @param int $maxSize max size of file, if the size is grater than this value the file will be truncated, 0 for no limit
*
* @return bool returns true if the file was created successfully
*/
public function addFileFromString($archivePath, $content, $maxSize = 0)
{
if ($maxSize > 0 && strlen($content) > $maxSize) {
$content = substr($content, 0, $maxSize);
}
$result = $this->zipArchive->addFromString($archivePath, $content);
if ($result && $this->encrypt) {
$this->zipArchive->setEncryptionName($archivePath, ZipArchive::EM_AES_256);
}
if ($result && !$this->compressed) {
$this->zipArchive->setCompressionName($archivePath, ZipArchive::CM_STORE);
}
return $result;
}
/**
* Open Zip archive, create it if don't exists
*
* @return bool|int Returns TRUE on success or the error code. See zip archive
*/
public function open()
{
if ($this->isOpened) {
return true;
}
if (($result = $this->zipArchive->open($this->archivePath, ZipArchive::CREATE)) === true) {
$this->isOpened = true;
if ($this->encrypt) {
$this->zipArchive->setPassword($this->password);
} else {
$this->zipArchive->setPassword('');
}
}
return $result;
}
/**
* Close zip archive
*
* @return bool True on success or false on failure.
*/
public function close()
{
if (!$this->isOpened) {
return true;
}
$result = false;
if (($result = $this->zipArchive->close()) === true) {
$this->isOpened = false;
}
return $result;
}
/**
* Get num files in zip archive
*
* @return int
*/
public function getNumFiles()
{
$this->open();
return $this->zipArchive->numFiles;
}
/**
* Get the value of compressed\
*
* @return bool
*/
public function isCompressed()
{
return $this->compressed;
}
/**
* Se compression if is available
*
* @param bool $compressed if true compress zip archive
*
* @return bool return compressd value
*/
public function setCompressed($compressed)
{
if (!method_exists($this->zipArchive, 'setCompressionName')) {
// If don't exists setCompressionName the archive can't create uncrompressed
$this->compressed = true;
} else {
$this->compressed = $compressed;
}
return $this->compressed;
}
/**
* Get the value of encrypt
*
* @return bool
*/
public function isEncrypted()
{
return $this->encrypt;
}
/**
* Return true if ZipArchive encryption is available
*
* @return bool
*/
public static function isEncryptionAvaliable()
{
static $isEncryptAvailable = null;
if ($isEncryptAvailable === null) {
if (!self::isPhpZipAvailable()) {
$isEncryptAvailable = false;
return false;
}
$zipArchive = new ZipArchive();
if (!method_exists($zipArchive, 'setEncryptionName')) {
$isEncryptAvailable = false;
return false;
}
if (version_compare(self::getLibzipVersion(), '1.2.0', '<')) {
$isEncryptAvailable = false;
return false;
}
$isEncryptAvailable = true;
}
return $isEncryptAvailable;
}
/**
* Get libzip version
*
* @return string
*/
public static function getLibzipVersion()
{
static $zlibVersion = null;
if (is_null($zlibVersion)) {
ob_start();
SnapUtil::phpinfo(INFO_MODULES);
$info = (string) ob_get_clean();
if (preg_match('/<td\s.*?>\s*(libzip.*\sver.+?)\s*<\/td>\s*<td\s.*?>\s*(.+?)\s*<\/td>/i', $info, $matches) !== 1) {
$zlibVersion = "0";
} else {
$zlibVersion = $matches[2];
}
}
return $zlibVersion;
}
/**
* Set encryption
*
* @param bool $encrypt true if archvie must be encrypted
* @param string $password password
* @return bool
*/
public function setEncrypt($encrypt, $password = '')
{
$this->encrypt = (self::isEncryptionAvaliable() ? $encrypt : false);
if ($this->encrypt) {
$this->password = $password;
} else {
$this->password = '';
}
if ($this->isOpened) {
if ($this->encrypt) {
$this->zipArchive->setPassword($this->password);
} else {
$this->zipArchive->setPassword('');
}
}
return $this->encrypt;
}
/**
* Files regex search and return zip file stat
*
* @param string $path Archive path
* @param string $regex Regex to search
* @param string $password Password if archive is encrypted or empty string
*
* @return false|array{name:string,index:int,crc:int,size:int,mtime:int,comp_size:int,comp_method:int}
*/
public static function searchRegex($path, $regex, $password = '')
{
if (!self::isPhpZipAvailable()) {
throw new Exception(__('ZipArchive PHP module is not installed/enabled. The current Backup cannot be opened.', 'duplicator'));
}
$zip = new ZipArchive();
if ($zip->open($path) !== true) {
throw new Exception('Cannot open the ZipArchive file. Please see the online FAQ\'s for additional help.' . $path);
}
if (strlen($password)) {
$zip->setPassword($password);
}
$result = false;
for ($i = 0; $i < $zip->numFiles; $i++) {
/** @var array{name:string,index:int,crc:int,size:int,mtime:int,comp_size:int,comp_method:int} */
$stat = $zip->statIndex($i);
$name = basename($stat['name']);
if (preg_match($regex, $name) === 1) {
$result = $stat;
break;
}
}
$zip->close();
return $result;
}
}

View File

@@ -0,0 +1,548 @@
<?php
namespace Duplicator\Views;
use Closure;
use DUP_Server;
use Duplicator\Core\MigrationMng;
use Duplicator\Utils\LinkManager;
use Duplicator\Libs\Snap\SnapUtil;
use Duplicator\Core\Controllers\ControllersManager;
use Duplicator\Core\Notifications\Notice;
use Duplicator\Core\Views\TplMng;
use Duplicator\Utils\Autoloader;
use Exception;
/**
* Admin Notices
*/
class AdminNotices
{
const OPTION_KEY_MIGRATION_SUCCESS_NOTICE = 'duplicator_migration_success';
const OPTION_KEY_ACTIVATE_PLUGINS_AFTER_INSTALL = 'duplicator_activate_plugins_after_installation';
//TEMPLATE VALUE: This is a just a simple example for setting up quick notices
const OPTION_KEY_NEW_NOTICE_TEMPLATE = 'duplicator_new_template_notice';
const OPTION_KEY_IS_ENABLE_NOTICE_DISMISSED = 'duplicator_is_enable_notice_dismissed';
const OPTION_KEY_IS_MU_NOTICE_DISMISSED = 'duplicator_is_mu_notice_dismissed';
const GEN_INFO_NOTICE = 0;
const GEN_SUCCESS_NOTICE = 1;
const GEN_WARNING_NOTICE = 2;
const GEN_ERROR_NOTICE = 3;
/**
* init notice actions
*
* @return void
*/
public static function init()
{
add_action('admin_init', array(__CLASS__, 'adminInit'));
add_action('admin_enqueue_scripts', array(__CLASS__, 'unhookThirdPartyNotices'), 99999, 1);
}
/**
* init notice actions
*
* @return void
*/
public static function adminInit()
{
$notices = array();
if (is_multisite()) {
$noCapabilitiesNotice = is_super_admin() && !current_user_can('export');
$notices[] = array(__CLASS__, 'multisiteNotice');
} else {
$noCapabilitiesNotice = in_array('administrator', $GLOBALS['current_user']->roles) && !current_user_can('export');
}
if ($noCapabilitiesNotice) {
$notices[] = array(__CLASS__, 'showNoExportCapabilityNotice');
}
if (is_multisite()) {
$displayNotices = is_super_admin() && current_user_can('export');
} else {
$displayNotices = current_user_can('export');
}
if ($displayNotices) {
$notices[] = array(__CLASS__, 'clearInstallerFilesAction'); // BEFORE MIGRATION SUCCESS NOTICE
$notices[] = array(__CLASS__, 'migrationSuccessNotice');
$notices[] = array(__CLASS__, 'installAutoDeactivatePlugins');
$notices[] = array(__CLASS__, 'failedOneClickUpgradeNotice');
}
$action = is_multisite() ? 'network_admin_notices' : 'admin_notices';
foreach ($notices as $notice) {
add_action($action, $notice);
}
}
/**
* Remove all notices coming from other plugins
*
* @param string $hook Hook string
*
* @return void
*/
public static function unhookThirdPartyNotices($hook)
{
if (!ControllersManager::isDuplicatorPage()) {
return;
}
global $wp_filter;
$filterHooks = array('user_admin_notices', 'admin_notices', 'all_admin_notices', 'network_admin_notices');
foreach ($filterHooks as $filterHook) {
if (empty($wp_filter[$filterHook]->callbacks) || !is_array($wp_filter[$filterHook]->callbacks)) {
continue;
}
foreach ($wp_filter[$filterHook]->callbacks as $priority => $hooks) {
foreach ($hooks as $name => $arr) {
if (is_object($arr['function']) && $arr['function'] instanceof Closure) {
unset($wp_filter[$filterHook]->callbacks[$priority][$name]);
continue;
}
if (
!empty($arr['function'][0]) &&
is_object($arr['function'][0]) &&
strpos(get_class($arr['function'][0]), Autoloader::ROOT_NAMESPACE) === 0
) {
continue;
}
if (!empty($name) && strpos($name, Autoloader::ROOT_NAMESPACE) !== 0) {
unset($wp_filter[$filterHook]->callbacks[$priority][$name]);
}
}
}
}
}
/**
* Clear installer file action
*
* @return void
*/
public static function clearInstallerFilesAction()
{
if (!\DUP_CTRL_Tools::isDiagnosticPage() || get_option(self::OPTION_KEY_MIGRATION_SUCCESS_NOTICE) == true) {
return;
}
if (sanitize_text_field(SnapUtil::filterInputRequest('action')) === 'installer') {
if (! wp_verify_nonce($_REQUEST['_wpnonce'], 'duplicator_cleanup_page')) {
echo '<p>' . __('Security issue', 'duplicator') . '</p>';
exit; // Get out of here bad nounce!
}
?>
<div id="message" class="notice notice-success">
<?php require DUPLICATOR_LITE_PATH . '/views/parts/migration-clean-installation-files.php'; ?>
</div>
<?php
}
}
/**
* Shows a display message in the wp-admin if any reserved files are found
*
* @return void
*/
public static function migrationSuccessNotice()
{
if (get_option(self::OPTION_KEY_MIGRATION_SUCCESS_NOTICE) != true) {
return;
}
if (\DUP_CTRL_Tools::isDiagnosticPage()) {
require DUPLICATOR_LITE_PATH . '/views/parts/migration-message.php';
} else {
require DUPLICATOR_LITE_PATH . '/views/parts/migration-almost-complete.php';
}
}
/**
* Shows a display message in the wp-admin if any reserved files are found
*
* @return string Html formatted text notice warnings
*/
public static function showReservedFilesNotice()
{
//Show only on Duplicator pages and Dashboard when plugin is active
$dup_active = is_plugin_active('duplicator/duplicator.php');
$dup_perm = current_user_can('manage_options');
if (!$dup_active || !$dup_perm) {
return;
}
$screen = get_current_screen();
if (!isset($screen)) {
return;
}
$is_installer_cleanup_req = ($screen->id == 'duplicator_page_duplicator-tools' && isset($_GET['action']) && $_GET['action'] == 'installer');
if (DUP_Server::hasInstallerFiles() && !$is_installer_cleanup_req) {
MigrationMng::renameInstallersPhpFiles();
$on_active_tab = isset($_GET['section']) ? $_GET['section'] : '';
echo '<div class="dup-updated notice notice-success dup-global-error-reserved-files" id="message"><p>';
//Safe Mode Notice
$safe_html = '';
if (get_option("duplicator_exe_safe_mode", 0) > 0) {
$safe_msg1 = __('Safe Mode:', 'duplicator');
$safe_msg2 = __('During the install safe mode was enabled deactivating all plugins.<br/> Please be sure to ', 'duplicator');
$safe_msg3 = __('re-activate the plugins', 'duplicator');
$safe_html = "<div class='notice-safemode'><b>{$safe_msg1}</b><br/>{$safe_msg2} <a href='plugins.php'>{$safe_msg3}</a>!</div><br/>";
}
//On Tools > Cleanup Page
if ($screen->id == 'duplicator_page_duplicator-tools' && ($on_active_tab == "info" || $on_active_tab == '')) {
$title = __('This site has been successfully migrated!', 'duplicator');
$msg1 = __('Final step(s):', 'duplicator');
$msg2 = __('This message will be removed after all installer files are removed. Installer files must be removed to maintain a secure site. '
. 'Click the link above or button below to remove all installer files and complete the migration.', 'duplicator');
echo "<b class='pass-msg'><i class='fa fa-check-circle'></i> " . esc_html($title) .
"</b> <br/> {$safe_html} <b>" . esc_html($msg1) . "</b> <br/>";
printf(
"1. <a href='javascript:void(0)' onclick='jQuery(\"#dup-remove-installer-files-btn\").click()'>%s</a><br/>",
esc_html__('Remove Installation Files Now!', 'duplicator')
);
printf(
"2. <a href='https://wordpress.org/support/plugin/duplicator/reviews/#new-post' target='wporg'>%s</a> <br/> ",
esc_html__('Optionally, Review Duplicator at WordPress.org...', 'duplicator')
);
echo "<div class='pass-msg'>" . esc_html($msg2) . "</div>";
//All other Pages
} else {
$title = __('Migration Almost Complete!', 'duplicator');
$msg = __(
'Reserved Duplicator installation files have been detected in the root directory. Please delete these installation files to '
. 'avoid security issues. <br/> Go to: Duplicator > Tools > General > Information > Utils and click the "Remove Installation Files" button',
'duplicator'
);
$nonce = wp_create_nonce('duplicator_cleanup_page');
$url = ControllersManager::getMenuLink(
ControllersManager::TOOLS_SUBMENU_SLUG,
'diagnostics',
null,
array(
'section' => 'info',
'_wpnonce' => $nonce,
),
true
);
echo "<b>{$title}</b><br/> {$safe_html} {$msg}";
@printf("<br/><a href='{$url}'>%s</a>", __('Take me there now!', 'duplicator'));
}
echo "</p></div>";
}
}
/**
* Shows a message for redirecting a page
*
* @param string $location The location to redirect to
*
* @return never
*/
public static function redirect($location)
{
echo '<div class="dup-redirect"><i class="fas fa-circle-notch fa-spin fa-fw"></i>';
esc_html__('Redirecting Please Wait...', 'duplicator');
echo '</div>';
echo "<script>window.location = '{$location}';</script>";
die(esc_html__('Invalid token permissions to perform this request.', 'duplicator'));
}
/**
* Shows install deactivated function
*
* @return void
*/
public static function installAutoDeactivatePlugins()
{
$reactivatePluginsAfterInstallation = get_option(self::OPTION_KEY_ACTIVATE_PLUGINS_AFTER_INSTALL, false);
$pluginsToActive = get_option(self::OPTION_KEY_ACTIVATE_PLUGINS_AFTER_INSTALL, false);
if (!is_array($pluginsToActive) || empty($pluginsToActive)) {
return false;
}
$shouldBeActivated = array();
$allPlugins = get_plugins();
foreach ($pluginsToActive as $index => $pluginSlug) {
if (!isset($allPlugins[$pluginSlug])) {
unset($pluginsToActive[$index]);
continue;
}
$isActive = is_plugin_active($pluginSlug);
if (!$isActive && isset($allPlugins[$pluginSlug])) {
$shouldBeActivated[$pluginSlug] = $allPlugins[$pluginSlug]['Name'];
} else {
unset($pluginsToActive[$index]);
}
}
if (empty($shouldBeActivated)) {
delete_option(self::OPTION_KEY_ACTIVATE_PLUGINS_AFTER_INSTALL);
return;
} else {
update_option(self::OPTION_KEY_ACTIVATE_PLUGINS_AFTER_INSTALL, $pluginsToActive);
}
$activatePluginsAnchors = array();
foreach ($shouldBeActivated as $slug => $title) {
$activateURL = wp_nonce_url(admin_url('plugins.php?action=activate&plugin=' . $slug), 'activate-plugin_' . $slug);
$anchorTitle = sprintf(esc_html__('Activate %s', 'duplicator'), $title);
$activatePluginsAnchors[] = '<a href="' . $activateURL . '"
title="' . esc_attr($anchorTitle) . '">' .
$title . '</a>';
}
?>
<div class="update-nag duplicator-plugin-activation-admin-notice notice notice-warning duplicator-admin-notice is-dismissible"
data-to-dismiss="<?php echo esc_attr(self::OPTION_KEY_ACTIVATE_PLUGINS_AFTER_INSTALL); ?>" >
<p>
<?php
echo "<b>" . esc_html__("Warning!", "duplicator") . "</b> " . esc_html__("Migration Almost Complete!", "duplicator") . " <br/>";
echo esc_html__(
"Plugin(s) listed here have been deactivated during installation to help prevent issues. Please activate them to finish this migration: ",
"duplicator"
) . "<br/>";
echo implode(' ,', $activatePluginsAnchors);
?>
</p>
</div>
<?php
}
/**
* Shows install deactivated function
*
* @return void
*/
public static function failedOneClickUpgradeNotice()
{
if (SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'action') !== 'upgrade_finalize_fail') {
return;
}
Notice::error(__('Upgrade failed. Please check if you have the necessary permissions to activate plugins.', 'duplicator'), 'upgrade_finalize_fail');
}
/**
* Shows feedback notices after certain no. of packages successfully created.
*
* @return void
*/
public static function showFeedBackNotice()
{
$notice_id = 'rate_us_feedback';
if (!current_user_can('manage_options')) {
return;
}
$notices = get_user_meta(get_current_user_id(), DUPLICATOR_ADMIN_NOTICES_USER_META_KEY, true);
if (empty($notices)) {
$notices = array();
}
$duplicator_pages = array(
'toplevel_page_duplicator',
'duplicator_page_duplicator-tools',
'duplicator_page_duplicator-settings',
'duplicator_page_duplicator-gopro',
);
if (!in_array(get_current_screen()->id, $duplicator_pages) || (isset($notices[$notice_id]) && 'true' === $notices[$notice_id])) {
return;
}
global $wpdb;
// not using DUP_Util::getTablePrefix() in place of $tablePrefix because AdminNotices included initially (Duplicator\Lite\Requirement
// is depended on the AdminNotices)
$tablePrefix = (is_multisite() && is_plugin_active_for_network('duplicator/duplicator.php')) ?
$wpdb->base_prefix :
$wpdb->prefix;
$tableName = esc_sql($tablePrefix . 'duplicator_packages');
$packagesCount = $wpdb->get_var("SELECT count(id) FROM `{$tableName}` WHERE status=100");
if ($packagesCount < DUPLICATOR_FEEDBACK_NOTICE_SHOW_AFTER_NO_PACKAGE) {
return;
}
$notices[$notice_id] = 'false';
update_user_meta(get_current_user_id(), DUPLICATOR_ADMIN_NOTICES_USER_META_KEY, $notices);
$dismiss_url = wp_nonce_url(
add_query_arg(array(
'action' => 'duplicator_set_admin_notice_viewed',
'notice_id' => esc_attr($notice_id),
), admin_url('admin-post.php')),
'duplicator_set_admin_notice_viewed',
'nonce'
);
?>
<div class="notice updated duplicator-message duplicator-message-dismissed" data-notice_id="<?php echo esc_attr($notice_id); ?>">
<div class="duplicator-message-inner">
<div class="duplicator-message-icon">
<img
src="<?php echo esc_url(DUPLICATOR_PLUGIN_URL . "assets/img/logo.png"); ?>"
style="text-align:top; margin:0; height:60px; width:60px;" alt="Duplicator">
</div>
<div class="duplicator-message-content">
<p>
<strong>
<?php echo __('Congrats!', 'duplicator'); ?>
</strong>
<?php
printf(
esc_html__(
'You created over %d backups with Duplicator. Great job! If you can spare a minute,
please help us by leaving a five star review on WordPress.org.',
'duplicator'
),
DUPLICATOR_FEEDBACK_NOTICE_SHOW_AFTER_NO_PACKAGE
); ?>
</p>
<p class="duplicator-message-actions">
<a
href="https://wordpress.org/support/plugin/duplicator/reviews/#new-post"
target="_blank" class="button button-primary duplicator-notice-rate-now"
>
<?php esc_html_e("Sure! I'd love to help", 'duplicator'); ?>
</a>
<a href="<?php echo esc_url($dismiss_url); ?>" class="button duplicator-notice-dismiss">
<?php esc_html_e('Hide Notification', 'duplicator'); ?>
</a>
</p>
</div>
</div>
</div>
<?php
}
/**
* Shows a display message in the wp-admin if the logged in user role has not export capability
*
* @return void
*/
public static function showNoExportCapabilityNotice()
{
if (is_admin() && in_array('administrator', $GLOBALS['current_user']->roles) && !current_user_can('export')) {
$faqUrl = esc_url(LinkManager::getDocUrl(
'how-to-resolve-duplicator-plugin-user-interface-ui-issues',
'admin_notice',
'duplicator menu missing'
));
$errorMessage = __(
'<strong>Duplicator</strong><hr> Your logged-in user role does not have export
capability so you don\'t have access to Duplicator functionality.',
'duplicator'
) .
"<br>" .
sprintf(
_x(
'<strong>RECOMMENDATION:</strong> Add export capability to your role. See FAQ: ' .
'%1$sWhy is the Duplicator/Packages menu missing from my admin menu?%2$s',
'%1$s and %2$s are <a> tags',
'duplicator'
),
'<a target="_blank" href="' . $faqUrl . '">',
'</a>'
);
self::displayGeneralAdminNotice($errorMessage, self::GEN_ERROR_NOTICE, true);
}
}
/**
* Display multisite notice
*
* @return void
*/
public static function multisiteNotice()
{
if (
!ControllersManager::isDuplicatorPage() ||
ControllersManager::isCurrentPage(ControllersManager::ABOUT_US_SUBMENU_SLUG) ||
ControllersManager::isCurrentPage(ControllersManager::SETTINGS_SUBMENU_SLUG, 'general')
) {
return;
}
$message = TplMng::getInstance()->render('parts/notices/drm_multisite_msg', array(), false);
self::displayGeneralAdminNotice($message, self::GEN_ERROR_NOTICE, false, ['duplicator-multisite-notice']);
}
/**
* display genral admin notice by printing it
*
* @param string $htmlMsg html code to be printed
* @param integer $noticeType constant value of SELF::GEN_
* @param boolean $isDismissible whether the notice is dismissable or not. Default is true
* @param array|string $extraClasses add more classes to the notice div
*
* @return void
*/
public static function displayGeneralAdminNotice($htmlMsg, $noticeType, $isDismissible = true, $extraClasses = array())
{
if (empty($extraClasses)) {
$classes = array();
} elseif (is_array($extraClasses)) {
$classes = $extraClasses;
} else {
$classes = array($extraClasses);
}
$classes[] = 'notice';
switch ($noticeType) {
case self::GEN_INFO_NOTICE:
$classes[] = 'notice-info';
break;
case self::GEN_SUCCESS_NOTICE:
$classes[] = 'notice-success';
break;
case self::GEN_WARNING_NOTICE:
$classes[] = 'notice-warning';
break;
case self::GEN_ERROR_NOTICE:
$classes[] = 'notice-error';
break;
default:
throw new Exception('Invalid Admin notice type!');
}
if ($isDismissible) {
$classes[] = 'is-dismissible';
}
$classesStr = implode(' ', $classes);
?>
<div class="<?php echo esc_attr($classesStr); ?>">
<p>
<?php
if (self::GEN_ERROR_NOTICE == $noticeType) {
?>
<i class='fa fa-exclamation-triangle'></i>
<?php
}
?>
<?php
echo $htmlMsg;
?>
</p>
</div>
<?php
}
}

View File

@@ -0,0 +1,291 @@
<?php
namespace Duplicator\Views;
use DUP_Package;
use DUP_PackageStatus;
use Duplicator\Core\Views\TplMng;
/**
* Dashboard widget
*/
class DashboardWidget
{
const LAST_PACKAGE_TIME_WARNING = 86400; // 24 hours
const LAST_PACKAGES_LIMIT = 3;
const RECOMMENDED_PLUGIN_ENABLED = true;
const RECOMMENDED_PLUGIN_DISMISSED_OPT_KEY = 'duplicator_recommended_plugin_dismissed';
/**
* Add the dashboard widget
*
* @return void
*/
public static function init()
{
if (is_multisite()) {
add_action('wp_network_dashboard_setup', array(__CLASS__, 'addDashboardWidget'));
} else {
add_action('wp_dashboard_setup', array(__CLASS__, 'addDashboardWidget'));
}
}
/**
* Render the dashboard widget
*
* @return void
*/
public static function addDashboardWidget()
{
if (!current_user_can('administrator')) {
return;
}
wp_add_dashboard_widget(
'duplicator_dashboard_widget',
__('Duplicator', 'duplicator'),
array(__CLASS__, 'renderContent')
);
}
/**
* Render the dashboard widget content
*
* @return void
*/
public static function renderContent()
{
TplMng::getInstance()->setStripSpaces(true);
?>
<div class="dup-dashboard-widget-content">
<?php self::renderPackageCreate(); ?>
<hr class="separator" >
<?php self::renderRecentlyPackages(); ?>
<hr class="separator" >
<?php
self::renderSections();
if (self::RECOMMENDED_PLUGIN_ENABLED) { // @phpstan-ignore-line
self::renderRecommendedPluginSection();
}
?>
</div>
<?php
}
/**
* Render the package create button
*
* @return void
*/
protected static function renderPackageCreate()
{
TplMng::getInstance()->render(
'parts/DashboardWidget/package-create-section',
array (
'lastBackupString' => self::getLastBackupString()
)
);
}
/**
* Render the last packages
*
* @return void
*/
protected static function renderRecentlyPackages()
{
/** @var DUP_Package[] */
$packages = DUP_Package::get_packages_by_status(
array(
array(
'op' => '>=',
'status' => DUP_PackageStatus::COMPLETE
)
),
self::LAST_PACKAGES_LIMIT,
0,
'created DESC'
);
$totalsIds = DUP_Package::get_ids_by_status(
array(
array(
'op' => '>=',
'status' => DUP_PackageStatus::COMPLETE
)
)
);
$failuresIds = DUP_Package::get_ids_by_status(
array(
array(
'op' => '<',
'status' => 0
)
)
);
TplMng::getInstance()->render(
'parts/DashboardWidget/recently-packages',
array(
'packages' => $packages,
'totalPackages' => count($totalsIds),
'totalFailures' => count($failuresIds)
)
);
}
/**
* Render Duplicate sections
*
* @return void
*/
protected static function renderSections()
{
TplMng::getInstance()->render(
'parts/DashboardWidget/sections-section',
array(
'numSchedules' => 0,
'numSchedulesEnabled' => 0,
'numTemplates' => 1,
'numStorages' => 1,
'nextScheduleString' => '',
'recoverDateString' => ''
)
);
}
/**
* Get the last backup string
*
* @return string HTML string
*/
public static function getLastBackupString()
{
if (DUP_Package::isPackageRunning()) {
return '<span class="spinner"></span> <b>' . esc_html__('A Backup is currently running.', 'duplicator') . '</b>';
}
/** @var DUP_Package[] */
$lastPackage = DUP_Package::get_packages_by_status(
array(
array(
'op' => '>=',
'status' => DUP_PackageStatus::COMPLETE
)
),
1,
0,
'created DESC'
);
if (empty($lastPackage)) {
return '<b>' . esc_html__('No Backups have been created yet.', 'duplicator') . '</b>';
}
$createdTime = date_i18n(get_option('date_format'), strtotime($lastPackage[0]->Created));
if ($lastPackage[0]->getPackageLife() > self::LAST_PACKAGE_TIME_WARNING) {
$timeDiffClass = 'maroon';
} else {
$timeDiffClass = 'green';
}
$timeDiff = sprintf(
_x('%s ago', '%s represents the time diff, eg. 2 days', 'duplicator'),
$lastPackage[0]->getPackageLife('human')
);
return '<b>' . $createdTime . '</b> ' .
" (" . '<span class="' . $timeDiffClass . '"><b>' .
$timeDiff .
'</b></span>' . ")";
}
/**
* Return randomly chosen one of recommended plugins.
*
* @return false|array{name: string,slug: string,more: string,pro: array{file: string}}
*/
protected static function getRecommendedPluginData()
{
$plugins = array(
'google-analytics-for-wordpress/googleanalytics.php' => array(
'name' => __('MonsterInsights', 'duplicator'),
'slug' => 'google-analytics-for-wordpress',
'more' => 'https://www.monsterinsights.com/',
'pro' => array(
'file' => 'google-analytics-premium/googleanalytics-premium.php',
),
),
'all-in-one-seo-pack/all_in_one_seo_pack.php' => array(
'name' => __('AIOSEO', 'duplicator'),
'slug' => 'all-in-one-seo-pack',
'more' => 'https://aioseo.com/',
'pro' => array(
'file' => 'all-in-one-seo-pack-pro/all_in_one_seo_pack.php',
),
),
'coming-soon/coming-soon.php' => array(
'name' => __('SeedProd', 'duplicator'),
'slug' => 'coming-soon',
'more' => 'https://www.seedprod.com/',
'pro' => array(
'file' => 'seedprod-coming-soon-pro-5/seedprod-coming-soon-pro-5.php',
),
),
'wp-mail-smtp/wp_mail_smtp.php' => array(
'name' => __('WP Mail SMTP', 'duplicator'),
'slug' => 'wp-mail-smtp',
'more' => 'https://wpmailsmtp.com/',
'pro' => array(
'file' => 'wp-mail-smtp-pro/wp_mail_smtp.php',
),
),
);
$installed = get_plugins();
foreach ($plugins as $id => $plugin) {
if (isset($installed[$id])) {
unset($plugins[$id]);
}
if (isset($installed[$plugin['pro']['file']])) {
unset($plugins[$id]);
}
}
return ($plugins ? $plugins[ array_rand($plugins) ] : false);
}
/**
* Recommended plugin block HTML.
*
* @return void
*/
public static function renderRecommendedPluginSection()
{
if (get_user_meta(get_current_user_id(), self::RECOMMENDED_PLUGIN_DISMISSED_OPT_KEY, true) != false) {
return;
}
$plugin = self::getRecommendedPluginData();
if (empty($plugin)) {
return;
}
$installUrl = wp_nonce_url(
self_admin_url('update.php?action=install-plugin&plugin=' . rawurlencode($plugin['slug'])),
'install-plugin_' . $plugin['slug']
);
TplMng::getInstance()->render(
'parts/DashboardWidget/recommended-section',
array(
'plugin' => $plugin,
'installUrl' => $installUrl,
)
);
}
}

View File

@@ -0,0 +1,134 @@
<?php
/**
* @package Duplicator
*/
namespace Duplicator\Views;
use Duplicator\Core\Views\TplMng;
use Duplicator\Utils\Upsell;
class EducationElements
{
const DUP_SETTINGS_FOOTER_CALLOUT_DISMISSED = 'duplicator_settings_footer_callout_dismissed';
const DUP_PACKAGES_BOTTOM_BAR_DISMISSED = 'duplicator_packages_bottom_bar_dismissed';
const DUP_EMAIL_SUBSCRIBED_OPT_KEY = 'duplicator_email_subscribed';
/**
* Init hooks
*
* @return void
*/
public static function init()
{
add_action('duplicator_settings_page_footer', array(__CLASS__, 'displayCalloutCTA'));
add_action('duplicator_scan_progress_header', array(__CLASS__, 'didYouKnow'));
add_action('duplicator_scan_progress_footer', array(__CLASS__, 'emailForm'));
add_action('duplicator_build_progress_header', array(__CLASS__, 'didYouKnow'));
add_action('duplicator_build_progress_footer', array(__CLASS__, 'emailForm'));
add_action('duplicator_build_success_footer', array(__CLASS__, 'emailForm'));
add_action('duplicator_before_packages_footer', array(__CLASS__, 'bottomBar'));
}
/**
* Display callout CTA
*
* @return void
*/
public static function displayCalloutCTA()
{
if (get_user_meta(get_current_user_id(), self::DUP_SETTINGS_FOOTER_CALLOUT_DISMISSED, false) == true) {
return;
}
TplMng::getInstance()->render('parts/Education/callout-cta');
}
/**
* Display did you know
*
* @return void
*/
public static function didYouKnow()
{
$features = Upsell::getProFeatureList();
TplMng::getInstance()->render('parts/Education/did-you-know-blurb', array(
'feature' => $features[array_rand($features)]
));
}
/**
* Display email form
*
* @return void
*/
public static function emailForm()
{
if (self::userIsSubscribed()) {
return;
}
TplMng::getInstance()->render('parts/Education/subscribe-form');
}
/**
* Display did you know
*
* @return void
*/
public static function bottomBar()
{
if (get_user_meta(get_current_user_id(), self::DUP_PACKAGES_BOTTOM_BAR_DISMISSED, false) == true) {
return;
}
$numberOfPackages = \DUP_Package::count_by_status(array(
array('op' => '=' , 'status' => \DUP_PackageStatus::COMPLETE )
));
if ($numberOfPackages < 1) {
return;
}
$features = self::getBottomBarFeatures();
TplMng::getInstance()->render('parts/Education/packages-bottom-bar', array(
'feature' => $features[array_rand($features)]
));
}
/**
* Get packages bottom bar feature list
*
* @return string[]
*/
private static function getBottomBarFeatures()
{
return array(
__('Scheduled Backups - Ensure that important data is regularly and consistently backed up, allowing for quick ' .
'and efficient recovery in case of data loss.', 'duplicator'),
__('Cloud Backups - Back up to Dropbox, FTP, Google Drive, OneDrive, or Amazon S3 and more for safe storage.', 'duplicator'),
__('Recovery Points - Recovery Points provides protection against mistakes and bad updates by letting you ' .
'quickly rollback your system to a known, good state.', 'duplicator'),
__('Secure File Encryption - Protect and secure the archive file with industry-standard AES-256 encryption', 'duplicator'),
__('Server to Server Import - Direct Backup import from source server or cloud storage using URL. No need ' .
'to download the Backup to your desktop machine first.', 'duplicator'),
__('File & Database Table Filters - Use file and database filters to pick and choose exactly what you want to ' .
'backup or transfer. No bloat!', 'duplicator'),
__('Large Site Support - Duplicator Pro has developed a new way to package backups especially tailored for ' .
'larger site. No server timeouts or other restrictions.', 'duplicator'),
__('Multisite Support - Duplicator Pro supports multisite network backup & migration. You can even install ' .
'a subsite as a standalone site.', 'duplicator'),
);
}
/**
* True if user is subscribed
*
* @return bool
*/
public static function userIsSubscribed()
{
return get_user_meta(get_current_user_id(), self::DUP_EMAIL_SUBSCRIBED_OPT_KEY, false);
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* @package Duplicator
*/
namespace Duplicator\Views;
use Duplicator\Core\Controllers\ControllersManager;
use Duplicator\Core\Views\TplMng;
class ViewHelper
{
/**
* Display Duplicator Logo on all pages
*
* @return void
*/
public static function adminLogoHeader()
{
if (!ControllersManager::isDuplicatorPage()) {
return;
}
TplMng::getInstance()->render('parts/admin-logo-header');
}
}