diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index fee3338f0..cae1fa971 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -7,19 +7,39 @@ on: release: description: 'Release version (e.g. 1.2.3)' required: true + prerelease: + description: 'Pre-release version (e.g. RC1, beta, etc...)' + required: false permissions: contents: write env: TAG: ${{ github.event.inputs.release }} + PRETAG: ${{ github.event.inputs.prerelease }} BRANCH: temp-release-${{ github.event.inputs.release }} jobs: build: runs-on: ubuntu-latest steps: - # ref and repository are required, otherwise repo could appear in detached head state + - name: Prepare vars + id: vars + uses: actions/github-script@v7 + with: + script: | + const full_tag = [ + process.env.TAG, + process.env.PRETAG + ].filter(Boolean).join('-'); + const branch = `temp-release-${full_tag}`; + const is_prerelease = !!process.env.PRETAG; + + core.setOutput('full_tag', full_tag ); + core.setOutput('branch', branch ); + core.setOutput('is_prerelease', is_prerelease ); + + # 'ref' and 'repository' are required, otherwise repo could appear in detached head state - name: Checkout uses: actions/checkout@v4 with: @@ -82,8 +102,8 @@ jobs: uses: EndBug/add-and-commit@v9 with: message: Cleanup files for release - new_branch: ${{ env.BRANCH }} - tag: ${{ env.TAG }} + new_branch: ${{ steps.vars.outputs.branch }} + tag: ${{ steps.vars.outputs.full_tag }} # generate SBOM that will be attached to a release as an artifact - name: Create SBOM @@ -99,11 +119,11 @@ jobs: id: draft_release uses: softprops/action-gh-release@v1 with: - name: "Release ${{ env.TAG }}" + name: "Release ${{ steps.vars.outputs.full_tag }}" body: "${{ steps.changelog.outputs.description }}" - tag_name: ${{ env.TAG }} + tag_name: ${{ steps.vars.outputs.full_tag }} draft: true - prerelease: false + prerelease: ${{ steps.vars.outputs.is_prerelease }} # attach SBOM to release - name: Upload SBOM to release @@ -127,4 +147,4 @@ jobs: # delete temporary release branch - name: Delete temporary release branch run: | - git push origin --delete ${{ env.BRANCH }} + git push origin --delete ${{ steps.vars.outputs.branch }} diff --git a/changelog.txt b/changelog.txt index 83b427e71..502198405 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,26 @@ == Changelog == += 4.0.0 = +* NEW - use custom database tables to store GCS file data. This increases plugin performance and will be used for future improvements. +* NEW - added filter `wp_stateless_get_file`, retrieves the GCS file data, should be used instead of getting `sm_cloud` postmeta directly. +* NEW - added filter `wp_stateless_get_file_sizes`, retrieves the GCS file data for image sizes, should be used instead of getting `sm_cloud` postmeta directly. +* NEW - added filter `wp_stateless_get_file_meta`, retrieves all GCS file meta data, should be used instead of getting `sm_cloud` postmeta directly. +* NEW - added filter `wp_stateless_get_file_meta_value`, retrieves the GCS file meta data by meta_key, should be used instead of getting `sm_cloud` postmeta directly. +* NEW - added setting allowing to change email for WP-Stateless notifications. +* NEW - added new Settings tab `Addons`, which contains the list of WP-Stateless Addons, which replace Compatibilities. +* NEW - added new Settings tab `Status`, which contains status and health information related to Google Cloud Storage and WP-Stateless. +* NEW - CLI command `wp stateless migrate` to list and operate data optimizations. +* NEW - configuration constant [`WP_STATELESS_POSTMETA`](https://stateless.udx.io/docs/constants/#wp_stateless_postmeta) allows to read the GCS file data from postmeta instead of the new custom database tables. +* NEW - configuration constant [`WP_STATELESS_BATCH_HEALTHCHECK_INTERVAL`](https://stateless.udx.io/docs/constants/#wp_stateless_batch_healthcheck_interval) defines an interval in minutes for periodical health checks of a batch background process (like data optimization). +* COMPATIBILITY - BuddyBoss Compatibility replaced with [WP-Stateless – BuddyBoss Platform Addon](https://wordpress.org/plugins/wp-stateless-buddyboss-platform-addon/). +* COMPATIBILITY - Elementor Compatibility replaced with [WP-Stateless – Elementor Website Builder Addon](https://wordpress.org/plugins/wp-stateless-elementor-website-builder-addon/). +* COMPATIBILITY - Gravity Form Compatibility does not support older version of Gravity Forms (< 2.3). +* ENHANCEMENT - Allow dismissing notices in Admin Panel only for logged in users. +* ENHANCEMENT - Updated `wp-background-processing` library from from 1.0.2 to 1.1.1. +* ENHANCEMENT - Updated `phpseclib` 3.0.34 to 3.0.37. +* FIX - proper use of infinite timeout in `set_time_limit` function to avoid issues with PHP 8.1 and above [#704](https://github.com/udx/wp-stateless/issues/704). + = 3.4.1 = -FIX - improve security while processing AJAX requests in Admin Panel +* FIX - improve security while processing AJAX requests in Admin Panel = 3.4.0 = * ENHANCEMENT - removed `udx/lib-settings` package dependency for security reasons. diff --git a/changes.md b/changes.md index e8821c80d..9160496e5 100644 --- a/changes.md +++ b/changes.md @@ -1,5 +1,25 @@ +#### 4.0.0 +* NEW - use custom database tables to store GCS file data. This increases plugin performance and will be used for future improvements. +* NEW - added filter `wp_stateless_get_file`, retrieves the GCS file data, should be used instead of getting `sm_cloud` postmeta directly. +* NEW - added filter `wp_stateless_get_file_sizes`, retrieves the GCS file data for image sizes, should be used instead of getting `sm_cloud` postmeta directly. +* NEW - added filter `wp_stateless_get_file_meta`, retrieves all GCS file meta data, should be used instead of getting `sm_cloud` postmeta directly. +* NEW - added filter `wp_stateless_get_file_meta_value`, retrieves the GCS file meta data by meta_key, should be used instead of getting `sm_cloud` postmeta directly. +* NEW - added setting allowing to change email for WP-Stateless notifications. +* NEW - added new Settings tab `Addons`, which contains the list of WP-Stateless Addons, which replace Compatibilities. +* NEW - added new Settings tab `Status`, which contains status and health information related to Google Cloud Storage and WP-Stateless. +* NEW - CLI command `wp stateless migrate` to list and operate data optimizations. +* NEW - configuration constant [`WP_STATELESS_POSTMETA`](https://stateless.udx.io/docs/constants/#wp_stateless_postmeta) allows to read the GCS file data from postmeta instead of the new custom database tables. +* NEW - configuration constant [`WP_STATELESS_BATCH_HEALTHCHECK_INTERVAL`](https://stateless.udx.io/docs/constants/#wp_stateless_batch_healthcheck_interval) defines an interval in minutes for periodical health checks of a batch background process (like data optimization). +* COMPATIBILITY - BuddyBoss Compatibility replaced with [WP-Stateless – BuddyBoss Platform Addon](https://wordpress.org/plugins/wp-stateless-buddyboss-platform-addon/). +* COMPATIBILITY - Elementor Compatibility replaced with [WP-Stateless – Elementor Website Builder Addon](https://wordpress.org/plugins/wp-stateless-elementor-website-builder-addon/). +* COMPATIBILITY - Gravity Form Compatibility does not support older version of Gravity Forms (< 2.3). +* ENHANCEMENT - Allow dismissing notices in Admin Panel only for logged in users. +* ENHANCEMENT - Updated `wp-background-processing` library from from 1.0.2 to 1.1.1. +* ENHANCEMENT - Updated `phpseclib` 3.0.34 to 3.0.37. +* FIX - proper use of infinite timeout in `set_time_limit` function to avoid issues with PHP 8.1 and above [#704](https://github.com/udx/wp-stateless/issues/704). + #### 3.4.1 -FIX - improve security while processing AJAX requests in Admin Panel +* FIX - improve security while processing AJAX requests in Admin Panel #### 3.4.0 * ENHANCEMENT - removed `udx/lib-settings` package dependency for security reasons. diff --git a/composer.lock b/composer.lock index b3e77e3b7..324e7223f 100644 --- a/composer.lock +++ b/composer.lock @@ -269,15 +269,15 @@ }, { "name": "udx/lib-ud-api-client", - "version": "1.2.3", + "version": "1.2.4", "source": { "type": "git", "url": "git@github.com:udx/lib-ud-api-client", - "reference": "1.2.3" + "reference": "1.2.4" }, "dist": { "type": "zip", - "url": "https://github.com/udx/lib-ud-api-client/archive/1.2.3.zip" + "url": "https://github.com/udx/lib-ud-api-client/archive/1.2.4.zip" }, "require": { "php": ">=5.3" @@ -312,15 +312,15 @@ }, { "name": "udx/lib-wp-bootstrap", - "version": "1.3.2", + "version": "1.3.3", "source": { "type": "git", "url": "git@github.com:udx/lib-wp-bootstrap", - "reference": "1.3.2" + "reference": "1.3.3" }, "dist": { "type": "zip", - "url": "https://github.com/udx/lib-wp-bootstrap/archive/1.3.2.zip" + "url": "https://github.com/udx/lib-wp-bootstrap/archive/1.3.3.zip" }, "require": { "php": ">=5.3" diff --git a/l10n.php b/l10n.php index be71628ba..eafa634a4 100644 --- a/l10n.php +++ b/l10n.php @@ -84,6 +84,8 @@ 'get_non_images_media_id_request_failed' => __('Get non Images Media ID: Request failed', ud_get_stateless_media()->domain ), 'regenerate_single_image_request_failed' => __('Regenerate single image: Request failed', ud_get_stateless_media()->domain ), - + + 'confirm' => __('Confirm', ud_get_stateless_media()->domain ), + 'cancel' => __('Cancel', ud_get_stateless_media()->domain ), ); diff --git a/lib/Google/composer.lock b/lib/Google/composer.lock index 60223301f..351a8bea8 100644 --- a/lib/Google/composer.lock +++ b/lib/Google/composer.lock @@ -893,16 +893,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.34", + "version": "3.0.37", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "56c79f16a6ae17e42089c06a2144467acc35348a" + "reference": "cfa2013d0f68c062055180dd4328cc8b9d1f30b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/56c79f16a6ae17e42089c06a2144467acc35348a", - "reference": "56c79f16a6ae17e42089c06a2144467acc35348a", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/cfa2013d0f68c062055180dd4328cc8b9d1f30b8", + "reference": "cfa2013d0f68c062055180dd4328cc8b9d1f30b8", "shasum": "" }, "require": { @@ -983,7 +983,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.34" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.37" }, "funding": [ { @@ -999,7 +999,7 @@ "type": "tidelift" } ], - "time": "2023-11-27T11:13:31+00:00" + "time": "2024-03-03T02:14:58+00:00" }, { "name": "psr/cache", diff --git a/lib/Google/vendor/composer/installed.json b/lib/Google/vendor/composer/installed.json index 943f52b55..761f6d17c 100644 --- a/lib/Google/vendor/composer/installed.json +++ b/lib/Google/vendor/composer/installed.json @@ -923,17 +923,17 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.34", - "version_normalized": "3.0.34.0", + "version": "3.0.37", + "version_normalized": "3.0.37.0", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "56c79f16a6ae17e42089c06a2144467acc35348a" + "reference": "cfa2013d0f68c062055180dd4328cc8b9d1f30b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/56c79f16a6ae17e42089c06a2144467acc35348a", - "reference": "56c79f16a6ae17e42089c06a2144467acc35348a", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/cfa2013d0f68c062055180dd4328cc8b9d1f30b8", + "reference": "cfa2013d0f68c062055180dd4328cc8b9d1f30b8", "shasum": "" }, "require": { @@ -951,7 +951,7 @@ "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." }, - "time": "2023-11-27T11:13:31+00:00", + "time": "2024-03-03T02:14:58+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -1016,7 +1016,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.34" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.37" }, "funding": [ { diff --git a/lib/Google/vendor/composer/installed.php b/lib/Google/vendor/composer/installed.php index 420722874..2b0615279 100644 --- a/lib/Google/vendor/composer/installed.php +++ b/lib/Google/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'google/apiclient', 'pretty_version' => 'dev-latest', 'version' => 'dev-latest', - 'reference' => 'fd648044ff982d46b7692c486068bf1bc9120dee', + 'reference' => 'cb005f7d32cc2da68e63ac4cc39bbd53a556d581', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -31,7 +31,7 @@ 'google/apiclient' => array( 'pretty_version' => 'dev-latest', 'version' => 'dev-latest', - 'reference' => 'fd648044ff982d46b7692c486068bf1bc9120dee', + 'reference' => 'cb005f7d32cc2da68e63ac4cc39bbd53a556d581', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -128,9 +128,9 @@ 'dev_requirement' => false, ), 'phpseclib/phpseclib' => array( - 'pretty_version' => '3.0.34', - 'version' => '3.0.34.0', - 'reference' => '56c79f16a6ae17e42089c06a2144467acc35348a', + 'pretty_version' => '3.0.37', + 'version' => '3.0.37.0', + 'reference' => 'cfa2013d0f68c062055180dd4328cc8b9d1f30b8', 'type' => 'library', 'install_path' => __DIR__ . '/../phpseclib/phpseclib', 'aliases' => array(), diff --git a/lib/Google/vendor/phpseclib/phpseclib/BACKERS.md b/lib/Google/vendor/phpseclib/phpseclib/BACKERS.md index 4ee6a4f9b..efca482ad 100644 --- a/lib/Google/vendor/phpseclib/phpseclib/BACKERS.md +++ b/lib/Google/vendor/phpseclib/phpseclib/BACKERS.md @@ -13,4 +13,5 @@ phpseclib ongoing development is made possible by [Tidelift](https://tidelift.co - [Rachel Fish](https://github.com/itsrachelfish) - Tharyrok - [cjhaas](https://github.com/cjhaas) -- [istiak-tridip](https://github.com/istiak-tridip) \ No newline at end of file +- [istiak-tridip](https://github.com/istiak-tridip) +- [Anna Filina](https://github.com/afilina) \ No newline at end of file diff --git a/lib/Google/vendor/phpseclib/phpseclib/README.md b/lib/Google/vendor/phpseclib/phpseclib/README.md index bbb1e9f06..37cbcb9d5 100644 --- a/lib/Google/vendor/phpseclib/phpseclib/README.md +++ b/lib/Google/vendor/phpseclib/phpseclib/README.md @@ -51,7 +51,7 @@ SSH-2, SFTP, X.509, an arbitrary-precision integer arithmetic library, Ed25519 / * PHP4 compatible * Composer compatible (PSR-0 autoloading) * Install using Composer: `composer require phpseclib/phpseclib:~1.0` -* [Download 1.0.21 as ZIP](http://sourceforge.net/projects/phpseclib/files/phpseclib1.0.21.zip/download) +* [Download 1.0.23 as ZIP](http://sourceforge.net/projects/phpseclib/files/phpseclib1.0.23.zip/download) ## Security contact information diff --git a/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/AsymmetricKey.php b/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/AsymmetricKey.php index 256c86906..09eb5b1b3 100644 --- a/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/AsymmetricKey.php +++ b/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/AsymmetricKey.php @@ -94,7 +94,7 @@ abstract class AsymmetricKey /** * @param string $type - * @return string + * @return array|string */ abstract public function toString($type, array $options = []); @@ -382,7 +382,7 @@ public static function addFileFormat($fullname) $shortname = $meta->getShortName(); self::$plugins[static::ALGORITHM]['Keys'][strtolower($shortname)] = $fullname; if ($meta->hasConstant('IS_INVISIBLE')) { - self::$invisiblePlugins[static::ALGORITHM] = strtolower($name); + self::$invisiblePlugins[static::ALGORITHM][] = strtolower($shortname); } } } diff --git a/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/SymmetricKey.php b/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/SymmetricKey.php index 00bfdd45c..175508d41 100644 --- a/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/SymmetricKey.php +++ b/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/SymmetricKey.php @@ -668,11 +668,13 @@ protected static function initialize_static_variables() switch (true) { // PHP_OS & "\xDF\xDF\xDF" == strtoupper(substr(PHP_OS, 0, 3)), but a lot faster case (PHP_OS & "\xDF\xDF\xDF") === 'WIN': - case !(is_string(php_uname('m')) && (php_uname('m') & "\xDF\xDF\xDF") == 'ARM'): + case !function_exists('php_uname'): + case !is_string(php_uname('m')): + case (php_uname('m') & "\xDF\xDF\xDF") != 'ARM': case defined('PHP_INT_SIZE') && PHP_INT_SIZE == 8: self::$use_reg_intval = true; break; - case is_string(php_uname('m')) && (php_uname('m') & "\xDF\xDF\xDF") == 'ARM': + case (php_uname('m') & "\xDF\xDF\xDF") == 'ARM': switch (true) { /* PHP 7.0.0 introduced a bug that affected 32-bit ARM processors: diff --git a/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/PrivateKey.php b/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/PrivateKey.php index 462ea1a33..598869614 100644 --- a/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/PrivateKey.php +++ b/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/PrivateKey.php @@ -150,7 +150,7 @@ public function sign($message) // we use specified curves to avoid issues with OpenSSL possibly not supporting a given named curve; // doing this may mean some curve-specific optimizations can't be used but idk if OpenSSL even // has curve-specific optimizations - $result = openssl_sign($message, $signature, $this->toString('PKCS8', ['namedCurve' => false]), $this->hash->getHash()); + $result = openssl_sign($message, $signature, $this->withPassword()->toString('PKCS8', ['namedCurve' => false]), $this->hash->getHash()); if ($result) { if ($shortFormat == 'ASN1') { diff --git a/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA.php b/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA.php index 135719979..19dcfea3f 100644 --- a/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA.php +++ b/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA.php @@ -332,6 +332,7 @@ public static function createKey($bits = 2048) openssl_pkey_export($rsa, $privatekeystr, null, $config); // clear the buffer of error strings stemming from a minimalistic openssl.cnf + // https://github.com/php/php-src/issues/11054 talks about other errors this'll pick up while (openssl_error_string() !== false) { } diff --git a/lib/Google/vendor/phpseclib/phpseclib/phpseclib/File/ASN1.php b/lib/Google/vendor/phpseclib/phpseclib/phpseclib/File/ASN1.php index 3096ff1a1..c4b06a560 100644 --- a/lib/Google/vendor/phpseclib/phpseclib/phpseclib/File/ASN1.php +++ b/lib/Google/vendor/phpseclib/phpseclib/phpseclib/File/ASN1.php @@ -1148,6 +1148,11 @@ public static function decodeOID($content) $oid = []; $pos = 0; $len = strlen($content); + // see https://github.com/openjdk/jdk/blob/2deb318c9f047ec5a4b160d66a4b52f93688ec42/src/java.base/share/classes/sun/security/util/ObjectIdentifier.java#L55 + if ($len > 4096) { + //throw new \RuntimeException("Object identifier size is limited to 4096 bytes ($len bytes present)"); + return false; + } if (ord($content[$len - 1]) & 0x80) { return false; diff --git a/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/Engine.php b/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/Engine.php index abdf3b475..474abe105 100644 --- a/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/Engine.php +++ b/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/Engine.php @@ -619,7 +619,7 @@ public function getLength() */ public function getLengthInBytes() { - return strlen($this->toBytes()); + return (int) ceil($this->getLength() / 8); } /** @@ -786,6 +786,11 @@ protected static function randomRangePrimeOuter(Engine $min, Engine $max) $min = $temp; } + $length = $max->getLength(); + if ($length > 8196) { + throw new \RuntimeException("Generation of random prime numbers larger than 8196 has been disabled ($length)"); + } + $x = static::randomRange($min, $max); return static::randomRangePrimeInner($x, $min, $max); @@ -990,6 +995,15 @@ protected function testPrimality($t) */ public function isPrime($t = false) { + // OpenSSL limits RSA keys to 16384 bits. The length of an RSA key is equal to the length of the modulo, which is + // produced by multiplying the primes p and q by one another. The largest number two 8196 bit primes can produce is + // a 16384 bit number so, basically, 8196 bit primes are the largest OpenSSL will generate and if that's the largest + // that it'll generate it also stands to reason that that's the largest you'll be able to test primality on + $length = $this->getLength(); + if ($length > 8196) { + throw new \RuntimeException("Primality testing is not supported for numbers larger than 8196 bits ($length)"); + } + if (!$t) { $t = $this->setupIsPrime(); } diff --git a/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP.php b/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP.php index 7e85783ef..2d8959522 100644 --- a/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP.php +++ b/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP.php @@ -1341,4 +1341,17 @@ protected static function testJITOnWindows() } return false; } + + /** + * Return the size of a BigInteger in bits + * + * @return int + */ + public function getLength() + { + $max = count($this->value) - 1; + return $max != -1 ? + $max * static::BASE + intval(ceil(log($this->value[$max] + 1, 2))) : + 0; + } } diff --git a/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Net/SFTP.php b/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Net/SFTP.php index cdf0bec69..144ef7950 100644 --- a/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Net/SFTP.php +++ b/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Net/SFTP.php @@ -547,7 +547,7 @@ private function precheck() */ private function partial_init_sftp_connection() { - $response = $this->openChannel(self::CHANNEL, true); + $response = $this->open_channel(self::CHANNEL, true); if ($response === true && $this->isTimeout()) { return false; } @@ -2129,8 +2129,8 @@ public function put($remote_file, $data, $mode = self::SOURCE_STRING, $start = - $offset = $start; } elseif ($mode & (self::RESUME | self::RESUME_START)) { // if NET_SFTP_OPEN_APPEND worked as it should _size() wouldn't need to be called - $size = $this->stat($remote_file)['size']; - $offset = $size !== false ? $size : 0; + $stat = $this->stat($remote_file); + $offset = $stat !== false && $stat['size'] ? $stat['size'] : 0; } else { $offset = 0; if ($this->version >= 5) { @@ -3295,6 +3295,7 @@ protected function reset_connection($reason) $this->use_request_id = false; $this->pwd = false; $this->requestBuffer = []; + $this->partial_init = false; } /** @@ -3445,7 +3446,7 @@ public function getSFTPLog() } /** - * Returns all errors + * Returns all errors on the SFTP layer * * @return array */ @@ -3455,7 +3456,7 @@ public function getSFTPErrors() } /** - * Returns the last error + * Returns the last error on the SFTP layer * * @return string */ diff --git a/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Net/SSH2.php b/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Net/SSH2.php index 57edc48cb..eee2e108d 100644 --- a/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Net/SSH2.php +++ b/lib/Google/vendor/phpseclib/phpseclib/phpseclib/Net/SSH2.php @@ -1102,10 +1102,22 @@ class SSH2 */ private $errorOnMultipleChannels; + /** + * Terrapin Countermeasure + * + * "During initial KEX, terminate the connection if any unexpected or out-of-sequence packet is received" + * -- https://github.com/openssh/openssh-portable/commit/1edb00c58f8a6875fad6a497aa2bacf37f9e6cd5 + * + * @var int + */ + private $extra_packets; + /** * Default Constructor. * * $host can either be a string, representing the host, or a stream resource. + * If $host is a stream resource then $port doesn't do anything, altho $timeout + * still will be used * * @param mixed $host * @param int $port @@ -1204,6 +1216,8 @@ public function __construct($host, $port = 22, $timeout = 10) ? \WeakReference::create($this) : $this; + $this->timeout = $timeout; + if (is_resource($host)) { $this->fsock = $host; return; @@ -1212,7 +1226,6 @@ public function __construct($host, $port = 22, $timeout = 10) if (Strings::is_stringable($host)) { $this->host = $host; $this->port = $port; - $this->timeout = $timeout; } } @@ -1536,7 +1549,7 @@ private function key_exchange($kexinit_payload_server = false) $preferred['client_to_server']['comp'] : SSH2::getSupportedCompressionAlgorithms(); - $kex_algorithms = array_merge($kex_algorithms, array('ext-info-c')); + $kex_algorithms = array_merge($kex_algorithms, ['ext-info-c', 'kex-strict-c-v00@openssh.com']); // some SSH servers have buggy implementations of some of the above algorithms switch (true) { @@ -1592,6 +1605,7 @@ private function key_exchange($kexinit_payload_server = false) if ($kexinit_payload_server === false) { $this->send_binary_packet($kexinit_payload_client); + $this->extra_packets = 0; $kexinit_payload_server = $this->get_binary_packet(); if ( @@ -1623,6 +1637,11 @@ private function key_exchange($kexinit_payload_server = false) $this->languages_server_to_client, $first_kex_packet_follows ) = Strings::unpackSSH2('L10C', $response); + if (in_array('kex-strict-s-v00@openssh.com', $this->kex_algorithms)) { + if ($this->session_id === false && $this->extra_packets) { + throw new \UnexpectedValueException('Possible Terrapin Attack detected'); + } + } $this->supported_private_key_algorithms = $this->server_host_key_algorithms; @@ -1881,6 +1900,10 @@ private function key_exchange($kexinit_payload_server = false) throw new \UnexpectedValueException('Expected SSH_MSG_NEWKEYS'); } + if (in_array('kex-strict-s-v00@openssh.com', $this->kex_algorithms)) { + $this->get_seq_no = $this->send_seq_no = 0; + } + $keyBytes = pack('Na*', strlen($keyBytes), $keyBytes); $this->encrypt = self::encryption_algorithm_to_crypt_instance($encrypt); @@ -2193,7 +2216,9 @@ private static function bad_algorithm_candidate($algorithm) */ public function login($username, ...$args) { - $this->auth[] = func_get_args(); + if (!$this->retry_connect) { + $this->auth[] = func_get_args(); + } // try logging with 'none' as an authentication method first since that's what // PuTTY does @@ -2815,7 +2840,7 @@ public function exec($command, callable $callback = null) // throw new \RuntimeException('If you want to run multiple exec()\'s you will need to disable (and re-enable if appropriate) a PTY for each one.'); //} - $this->openChannel(self::CHANNEL_EXEC); + $this->open_channel(self::CHANNEL_EXEC); if ($this->request_pty === true) { $terminal_modes = pack('C', NET_SSH2_TTY_OP_END); @@ -2912,7 +2937,7 @@ public function getOpenChannelCount() * @param bool $skip_extended * @return bool */ - protected function openChannel($channel, $skip_extended = false) + protected function open_channel($channel, $skip_extended = false) { if (isset($this->channel_status[$channel]) && $this->channel_status[$channel] != NET_SSH2_MSG_CHANNEL_CLOSE) { throw new \RuntimeException('Please close the channel (' . $channel . ') before trying to open it again'); @@ -2969,7 +2994,7 @@ public function openShell() throw new InsufficientSetupException('Operation disallowed prior to login()'); } - $this->openChannel(self::CHANNEL_SHELL); + $this->open_channel(self::CHANNEL_SHELL); $terminal_modes = pack('C', NET_SSH2_TTY_OP_END); $packet = Strings::packSSH2( @@ -3217,7 +3242,7 @@ public function write($cmd, $channel = null) */ public function startSubsystem($subsystem) { - $this->openChannel(self::CHANNEL_SUBSYSTEM); + $this->open_channel(self::CHANNEL_SUBSYSTEM); $packet = Strings::packSSH2( 'CNsCs', @@ -3319,11 +3344,38 @@ public function __destruct() /** * Is the connection still active? * + * $level has 3x possible values: + * 0 (default): phpseclib takes a passive approach to see if the connection is still active by calling feof() + * on the socket + * 1: phpseclib takes an active approach to see if the connection is still active by sending an SSH_MSG_IGNORE + * packet that doesn't require a response + * 2: phpseclib takes an active approach to see if the connection is still active by sending an SSH_MSG_CHANNEL_OPEN + * packet and imediately trying to close that channel. some routers, in particular, however, will only let you + * open one channel, so this approach could yield false positives + * + * @param int $level * @return bool */ - public function isConnected() + public function isConnected($level = 0) { - return ($this->bitmap & self::MASK_CONNECTED) && is_resource($this->fsock) && !feof($this->fsock); + if (!is_int($level) || $level < 0 || $level > 2) { + throw new \InvalidArgumentException('$level must be 0, 1 or 2'); + } + + if ($level == 0) { + return ($this->bitmap & self::MASK_CONNECTED) && is_resource($this->fsock) && !feof($this->fsock); + } + try { + if ($level == 1) { + $this->send_binary_packet(pack('CN', NET_SSH2_MSG_IGNORE, 0)); + } else { + $this->open_channel(self::CHANNEL_KEEP_ALIVE); + $this->close_channel(self::CHANNEL_KEEP_ALIVE); + } + return true; + } catch (\Exception $e) { + return false; + } } /** @@ -3396,7 +3448,7 @@ public function ping() } try { - $this->openChannel(self::CHANNEL_KEEP_ALIVE); + $this->open_channel(self::CHANNEL_KEEP_ALIVE); } catch (\RuntimeException $e) { return $this->reconnect(); } @@ -3509,6 +3561,11 @@ private function get_binary_packet($skip_channel_filter = false) } $start = microtime(true); + if ($this->curTimeout) { + $sec = (int) floor($this->curTimeout); + $usec = (int) (1000000 * ($this->curTimeout - $sec)); + stream_set_timeout($this->fsock, $sec, $usec); + } $raw = stream_get_contents($this->fsock, $this->decrypt_block_size); if (!strlen($raw)) { @@ -3767,9 +3824,11 @@ private function filter($payload, $skip_channel_filter) $this->bitmap = 0; return false; case NET_SSH2_MSG_IGNORE: + $this->extra_packets++; $payload = $this->get_binary_packet($skip_channel_filter); break; case NET_SSH2_MSG_DEBUG: + $this->extra_packets++; Strings::shift($payload, 2); // second byte is "always_display" list($message) = Strings::unpackSSH2('s', $payload); $this->errors[] = "SSH_MSG_DEBUG: $message"; @@ -3778,6 +3837,7 @@ private function filter($payload, $skip_channel_filter) case NET_SSH2_MSG_UNIMPLEMENTED: return false; case NET_SSH2_MSG_KEXINIT: + // this is here for key re-exchanges after the initial key exchange if ($this->session_id !== false) { if (!$this->key_exchange($payload)) { $this->bitmap = 0; @@ -4699,7 +4759,9 @@ private static function array_intersect_first(array $array1, array $array2) } /** - * Returns all errors + * Returns all errors / debug messages on the SSH layer + * + * If you are looking for messages from the SFTP layer, please see SFTP::getSFTPErrors() * * @return string[] */ @@ -4709,7 +4771,9 @@ public function getErrors() } /** - * Returns the last error + * Returns the last error received on the SSH layer + * + * If you are looking for messages from the SFTP layer, please see SFTP::getLastSFTPError() * * @return string */ diff --git a/lib/classes/batch/class-batch-task-manager.php b/lib/classes/batch/class-batch-task-manager.php new file mode 100644 index 000000000..1659d48c1 --- /dev/null +++ b/lib/classes/batch/class-batch-task-manager.php @@ -0,0 +1,351 @@ +path('lib/ns-vendor/classes/deliciousbrains/wp-background-processing/classes/wp-async-request.php', 'dir'); +} + +if (!class_exists('UDX_WP_Background_Process')) { + require_once ud_get_stateless_media()->path('lib/ns-vendor/classes/deliciousbrains/wp-background-processing/classes/wp-background-process.php', 'dir'); +} + +use wpCloud\StatelessMedia\Helper; +use wpCloud\StatelessMedia\Singleton; + +class BatchTaskManager extends \UDX_WP_Background_Process { + use Singleton; + + const STATE_KEY = '_state'; + const UPDATED_KEY = '_updated'; + const HEALTH_CHECK_INTERVAL = 60 * 5; // 5 minute + + protected $prefix = 'sm'; + protected $action = 'batch_process'; + + protected function __construct() { + parent::__construct(); + + $this->_init_hooks(); + $this->_check_force_continue(); + } + + private function _init_hooks() { + add_filter('wp_stateless_batch_state', [$this, 'get_state'], 10, 1); + add_filter('wp_stateless_batch_action_pause', [$this, 'pause_task'], 10, 2); + add_filter('wp_stateless_batch_action_resume', [$this, 'resume_task'], 10, 2); + add_filter('heartbeat_send', [$this, 'check_running_batch'], 10, 1 ); + } + + /** + * Check if we should force dispatch + * Check if the task is in progress and if the state was not updated last 5 minutes - try to continue the task + */ + private function _check_force_continue() { + $last_updated = $this->_get_last_updated(); + + if ( empty($last_updated) || $this->is_paused() ) { + return; + } + + $check_interval = self::HEALTH_CHECK_INTERVAL; + + if ( defined('WP_STATELESS_BATCH_HEALTHCHECK_INTERVAL') ) { + $check_interval = max($check_interval, WP_STATELESS_BATCH_HEALTHCHECK_INTERVAL * 60); + } + + if ( time() - $last_updated <= $check_interval ) { + return; + } + + Helper::log('Batch task freezed, trying to continue...'); + + // Forcing continue + $this->unlock_process(); + $this->handle(); + } + + /** + * Update current task state + * + * @return array + */ + private function _update_state($state) { + update_site_option( $this->identifier . self::STATE_KEY, $state ); + update_site_option( $this->identifier . self::UPDATED_KEY, time() ); + } + + /** + * Get current task state + * + * @return array + */ + private function _get_state() { + return get_site_option( $this->identifier . self::STATE_KEY, [] ); + } + + /** + * Get last state update of the current task + * + * @return int|null + */ + private function _get_last_updated() { + return get_site_option( $this->identifier . self::UPDATED_KEY, null ); + } + + /** + * Delete current task state + * + * @return array + */ + private function _delete_state() { + delete_site_option( $this->identifier . self::STATE_KEY ); + delete_site_option( $this->identifier . self::UPDATED_KEY ); + } + + /** + * Add new batch to the queue + * + * @param array $batch + */ + private function _add_batch($batch) { + if ( !empty($batch) ) { + $this->data( $batch )->save(); + } + } + + /** + * Get task object + * + * @param string $state|null + * @return IBatchTask + * @throws \Exception + */ + private function _get_batch_task_object($state = null) { + if ( empty($state) ) { + $state = $this->_get_state(); + } + + if ( !isset($state['class']) || !isset($state['file']) ) { + throw new \Exception("Can not get batch task file and class"); + } + + $class = $state['class']; + + if ( !class_exists($class) ) { + require_once $state['file']; + } + + $object = new $class(); + + if ( !is_a($object, '\wpCloud\StatelessMedia\Batch\IBatchTask') ) { + throw new \Exception("Batch task $class is not valid"); + } + + $object->set_state($state); + + return $object; + } + + /** + * Start the batch task + * + * @param string $class + * @param string|null $file + * @param string $email + */ + public function start_task($class, $file = null, $email = '') { + try { + // Prepare default state + $defaults = [ + 'class' => $class, + 'file' => $file, + 'email' => $email, + ]; + + $task_object = $this->_get_batch_task_object($defaults); + $task_object->init_state(); + + // Batch should be run prior to 'get_state' because it mutates the state + $this->_add_batch( $task_object->get_batch() ); + + // Save state + $state = wp_parse_args($task_object->get_state(), $defaults); + + $this->_update_state( $state ); + + Helper::log('Batch task started: ' . $class); + + do_action('wp_stateless_batch_task_started', $class, $file); + } catch (\Throwable $e) { + Helper::log("Batch task $class failed to start: " . $e->getMessage()); + + do_action('wp_stateless_batch_task_failed', $class, $file, $e->getMessage()); + + return; + } + + $this->dispatch(); + } + + /** + * Process batch task item. + * Returns false to remove item from queue + * Returns $item to repeat + * + * @param string $item + * @return bool|mixed + */ + public function task($item) { + $result = false; + + try { + $object = $this->_get_batch_task_object(); + + $result = $object->process_item($item); + $this->_update_state( $object->get_state() ); + + $result = apply_filters('wp_stateless_batch_task_item_processed', $result, $item); + } catch (\Throwable $e) { + Helper::log( "Batch task unable to handle item $item: " . $e->getMessage() ); + + $result = apply_filters('wp_stateless_batch_task_item_failed', $result, $item); + } + + return $result; + } + + /** + * Complete the batch task. Tries to get the next batch and continue + */ + protected function complete() { + $class = ''; + $description = 'Batch process'; + + // Check if we have more batched to run + try { + $object = $this->_get_batch_task_object(); + $class = $object::class; + $batch = $object->get_batch(); + $description = $object->get_description(); + + if ( !empty($batch) ) { + $this->_add_batch( $batch ); + $this->_update_state( $object->get_state() ); + + $this->dispatch(); + + return; + } + + Helper::log( 'Batch task completed: ' . $object::class ); + } catch (\Throwable $e) { + Helper::log( "Unable to process next batch: " . $e->getMessage() ); + } + + // If no more batches - delete state + $state = $this->_get_state(); + + parent::complete(); + $this->_delete_state(); + + do_action('wp_stateless_batch_task_finished', $class); + + $site = site_url(); + $subject = sprintf( __('Stateless %s complete successfully', ud_get_stateless_media()->domain), $description ); + $message = sprintf( + __("This is a simple notification to inform you that the WP-Stateless plugin has finished a %s process for %s.\n\nIf you have WP_DEBUG_LOG enabled, check those logs to review any errors that may have occurred during the process.", ud_get_stateless_media()->domain), + $description, + $site + ); + + do_action('wp_stateless_send_admin_email', $subject, $message, $state['email'] ?? ''); + } + + /** + * Check if batch task has a state, so it is in progress + * Because is_processing is true only while processing an item + * + * @param array|null $state + * @return bool + */ + public function is_running($state = null) { + if ( empty($state) ) { + $state = $this->_get_state(); + } + + return !empty($state); + } + + /** + * Get the state of the current batch process + * + * @param mixed $status + * @return mixed + */ + public function get_state($state) { + $state = $this->_get_state(); + + unset($state['class']); + unset($state['file']); + + $state['is_running'] = $this->is_running($state); + $state['is_paused'] = $this->is_paused(); + + return $state; + } + + /** + * Pause the batch task + * + * @param array $state + * @param array $params + * @return array + */ + public function pause_task($state, $params) { + $this->pause(); + + return apply_filters('wp_stateless_batch_state', $state, []); + } + + /** + * Resume the batch task + * + * @param array $state + * @param array $params + * @return array + */ + public function resume_task($state, $params) { + $this->resume(); + + return apply_filters('wp_stateless_batch_state', $state, []); + } + + /** + * Get the state key + * + * @return string + */ + public function get_state_key() { + return $this->identifier . self::STATE_KEY; + } + + /** + * Check if batch is running during WP heartbeat request + * + * @return array + */ + public function check_running_batch($response) { + if ( $this->is_running() ) { + $response['stateless-batch-running'] = true; + } + + return $response; + } +} diff --git a/lib/classes/batch/class-batch-task.php b/lib/classes/batch/class-batch-task.php new file mode 100644 index 000000000..a4bbc4a55 --- /dev/null +++ b/lib/classes/batch/class-batch-task.php @@ -0,0 +1,157 @@ +started = time(); + } + + /** + * Get human-friendly description + * + * @return string + */ + public function get_description() { + return $this->description; + } + + /** + * Get task state + * + * @return array + */ + public function get_state() { + if ( empty($this->started) ) { + $this->init_state(); + } + + // Calling process can add extra data to the state, so we should try to keep it + return wp_parse_args([ + 'id' => $this->id, + 'description' => $this->description, + 'total' => $this->total, + 'completed' => $this->completed, + 'limit' => $this->limit, + 'offset' => $this->offset, + 'started' => $this->started, + ], $this->state); + } + + /** + * Restore task state for processing between calls + * + * @param array $state + * @return array + */ + public function set_state($state) { + // Calling process can add extra data to the state, so we should try to keep it + $this->state = $state; + + // Restore state properties required for processing + if ( isset($state['description']) ) { + $this->description = $state['description']; + } + + if ( isset($state['started']) ) { + $this->started = $state['started']; + } + + if ( isset($state['total']) ) { + $this->total = $state['total']; + } + + if ( isset($state['completed']) ) { + $this->completed = $state['completed']; + } + + if ( isset($state['limit']) ) { + $this->limit = $state['limit']; + } + + if ( isset($state['offset']) ) { + $this->offset = $state['offset']; + } + } + + /** + * Get batch of data to process. False - no more data + * + * @return array|false + */ + abstract public function get_batch(); + + /** + * Process single item. If returns false - the item is removed from the queue. Otherwise repeated. + * + * @param mixed $item + * @return mixed|false + */ + abstract public function process_item($item); +} diff --git a/lib/classes/batch/class-migration.php b/lib/classes/batch/class-migration.php new file mode 100644 index 000000000..6a22d4891 --- /dev/null +++ b/lib/classes/batch/class-migration.php @@ -0,0 +1,28 @@ +id; + $state['is_migration'] = true; + + return $state; + } + + /** + * Can be used to test if the migration should run + * For example, if there are any old data that needs to be migrated + */ + public function should_run() { + return true; + } +} diff --git a/lib/classes/batch/interface-batch.php b/lib/classes/batch/interface-batch.php new file mode 100644 index 000000000..7125e366b --- /dev/null +++ b/lib/classes/batch/interface-batch.php @@ -0,0 +1,41 @@ +errors->add([ 'key' => $id, - 'title' => sprintf(__("%s: Addon for %s is recommended.", ud_get_stateless_media()->domain), ud_get_stateless_media()->name, $title), - 'button' => __("Get Addon", ud_get_stateless_media()->domain), + 'title' => sprintf(__('WP-Stateless: Install the %s Addon', ud_get_stateless_media()->domain), $title), + 'button' => __('Download Addon', ud_get_stateless_media()->domain), 'button_link' => admin_url('upload.php?page=stateless-settings&tab=stless_addons_tab'), - 'message' => __("Addon is recommended to ensure the functionality will work properly between {$title} and WP-Stateless.", ud_get_stateless_media()->domain), + 'message' => sprintf(__('Download and activate the WP-Stateless addon for %s to ensure compatibility.', ud_get_stateless_media()->domain), $title), ], 'notice'); } } diff --git a/lib/classes/class-ajax.php b/lib/classes/class-ajax.php index 83f014650..9b441e24c 100644 --- a/lib/classes/class-ajax.php +++ b/lib/classes/class-ajax.php @@ -50,6 +50,10 @@ public function __construct() { public function request() { check_ajax_referer('sm_inline_sync'); + if ( !is_user_logged_in() ) { + wp_send_json_error( array( 'error' => __( 'You are not allowed to do this action.', ud_get_stateless_media()->domain ) ) ); + } + global $doing_manual_sync; $response = array( diff --git a/lib/classes/class-api.php b/lib/classes/class-api.php index 6d4e0de12..263d49067 100644 --- a/lib/classes/class-api.php +++ b/lib/classes/class-api.php @@ -274,6 +274,55 @@ static public function syncStop(\WP_REST_Request $request) { return new \WP_Error('internal_server_error', $e->getMessage(), ['status' => 500]); } } + + /** + * Batch Status Endpoint. + * + * @param \WP_REST_Request $request + * @return \WP_REST_Response + */ + static public function batchState(\WP_REST_Request $request) { + try { + return new \WP_REST_Response(array( + 'ok' => true, + 'data' => apply_filters('wp_stateless_batch_state', [], $request->get_params()), + )); + } catch (\Throwable $e) { + return new \WP_Error('internal_server_error', $e->getMessage(), ['status' => 500]); + } + } + + /** + * Batch action (start/pause/resume) + * + * @param \WP_REST_Request $request + * @return \WP_Error|\WP_REST_Response + */ + static public function batchAction(\WP_REST_Request $request) { + try { + if (!user_can(self::$tokenData->user_id, 'manage_options')) { + return new \WP_Error('not_allowed', 'Sorry, you are not allowed to perform this action', ['status' => 403]); + } + + wp_set_current_user(self::$tokenData->user_id); + + $params = wp_parse_args($request->get_params(), [ + 'action' => '', + ]); + + if ( empty($params['action']) ) { + throw new \Exception('Batch action not set'); + } + + $action = $params['action']; + + return new \WP_REST_Response(array( + 'data' => apply_filters("wp_stateless_batch_action_$action", [], $params), + )); + } catch (\Throwable $e) { + return new \WP_Error('internal_server_error', $e->getMessage(), ['status' => 500]); + } + } } } } diff --git a/lib/classes/class-bootstrap.php b/lib/classes/class-bootstrap.php index dab3d83a2..262e1b715 100644 --- a/lib/classes/class-bootstrap.php +++ b/lib/classes/class-bootstrap.php @@ -13,6 +13,7 @@ use wpCloud\StatelessMedia\Sync\FileSync; use wpCloud\StatelessMedia\Sync\ImageSync; use wpCloud\StatelessMedia\Sync\NonLibrarySync; + use wpCloud\StatelessMedia\Batch\BatchTaskManager; if (!class_exists('wpCloud\StatelessMedia\Bootstrap')) { @@ -96,6 +97,7 @@ protected function __construct($args) { * $this->set( 'sm.client_id', 'zxcvv12adffse' ); */ $this->settings = new Settings($this); + Status::instance(); } /** @@ -108,10 +110,19 @@ public function load_textdomain() { * Instantiate class. */ public function init() { - // Parse feature falgs, set constants. + // Parse feature flags, set constants. $this->parse_feature_flags(); $sm_mode = $this->get('sm.mode'); + /** + * Send admin email + */ + add_action('wp_stateless_send_admin_email', array($this, 'send_admin_email'), 10, 3); + + // Should be created unconditionally and as early as possible to handle batch migration requests + BatchTaskManager::instance(); + Migrator::instance(); + new SyncNonMedia(); ImageSync::instance(); @@ -691,7 +702,9 @@ public function add_custom_row_actions($actions, $post, $detached) { if (!current_user_can('upload_files')) return $actions; - $sm_cloud = get_post_meta($post->ID, 'sm_cloud', 1); + $sm_cloud = apply_filters('wp_stateless_get_file', [], $post->ID); + + $sm_mode = $this->get('sm.mode'); if (!empty($sm_cloud) && $sm_mode === 'stateless') return $actions; @@ -757,7 +770,19 @@ public function api_init() { 'callback' => array($api_namespace, 'syncStop'), 'permission_callback' => array($api_namespace, 'authCheck') )); - } + + register_rest_route($route_namespace, '/batch/state', array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array($api_namespace, 'batchState'), + 'permission_callback' => array($api_namespace, 'authCheck') + )); + + register_rest_route($route_namespace, '/batch/action', array( + 'methods' => \WP_REST_Server::CREATABLE, + 'callback' => array($api_namespace, 'batchAction'), + 'permission_callback' => array($api_namespace, 'authCheck') + )); + } /** * Metabox for media modal page @@ -811,7 +836,7 @@ public function attachment_meta_box_callback($meta_boxes) { */ private function _prepare_data_for_metabox($meta_boxes, $post_id) { $post = get_post($post_id); - $sm_cloud = get_post_meta($post_id, 'sm_cloud', 1); + $sm_cloud = apply_filters('wp_stateless_get_file', [], $post_id, true); $sm_mode = $this->get('sm.mode'); if (empty($post)) { @@ -1231,6 +1256,7 @@ public function admin_init() { wp_register_style('wp-stateless-settings', $this->path('static/styles/wp-stateless-settings.css', 'url'), array(), self::$version); wp_register_style('wp-stateless-addons', $this->path('static/styles/wp-stateless-addons.css', 'url'), array(), self::$version); + wp_register_style('wp-stateless-status', $this->path('static/styles/wp-stateless-status.css', 'url'), array(), self::$version); // Sync tab wp_register_script('wp-stateless', ud_get_stateless_media()->path('static/scripts/wp-stateless.js', 'url'), array('jquery-ui-core', 'wp-stateless-settings', 'wp-api-request'), self::$version, true); @@ -1249,6 +1275,13 @@ public function admin_init() { $settings['key_json'] = "Currently configured via a constant."; } wp_localize_script('wp-stateless', 'wp_stateless_settings', $settings); + + // Batch processes tab + wp_register_script('wp-stateless-batch', ud_get_stateless_media()->path('static/scripts/wp-stateless-batch.js', 'url'), array('wp-api-request'), self::$version, true); + wp_localize_script('wp-stateless-batch', 'wp_stateless_batch', array( + 'REST_API_TOKEN' => Utility::generate_jwt_token(['user_id' => get_current_user_id()], DAY_IN_SECONDS), + 'is_running' => BatchTaskManager::instance()->is_running(), + )); } /** @@ -1305,10 +1338,16 @@ public function admin_enqueue_scripts($hook) { wp_enqueue_script('wp-stateless-select2'); wp_enqueue_style('wp-stateless-settings'); wp_enqueue_style('wp-stateless-addons'); + wp_enqueue_style('wp-stateless-status'); // Sync tab wp_enqueue_script('wp-stateless'); + // Data updates + wp_enqueue_script('jquery-ui-dialog'); + wp_enqueue_style('wp-jquery-ui-dialog'); + wp_enqueue_script('wp-stateless-batch'); + wp_enqueue_style('wp-pointer'); wp_enqueue_script('wp-pointer'); @@ -1340,8 +1379,8 @@ public function admin_enqueue_scripts($hook) { * @return mixed */ public function wp_get_attachment_image_attributes($attr, $attachment, $size = null) { - - $sm_cloud = get_post_meta($attachment->ID, 'sm_cloud', true); + + $sm_cloud = apply_filters('wp_stateless_get_file', [], $attachment->ID); if (is_array($sm_cloud) && !empty($sm_cloud['name'])) { $attr['class'] = $attr['class'] . ' wp-stateless-item'; $attr['data-image-size'] = is_array($size) ? implode('x', $size) : $size; @@ -1411,7 +1450,7 @@ public function image_downsize($false, $id, $size) { * @author korotkov@ud */ if (!$width && !$height) { - $sm_cloud = get_post_meta($id, 'sm_cloud', true); + $sm_cloud = apply_filters('wp_stateless_get_file', [], $id, true); if (is_string($size) && !empty($sm_cloud['sizes']) && !empty($sm_cloud['sizes'][$size])) { global $_wp_additional_image_sizes; @@ -1461,7 +1500,7 @@ public function wp_get_attachment_metadata($metadata, $attachment_id) { global $default_dir; $default_dir = false; /* Determine if the media file has GS data at all. */ - $sm_cloud = get_post_meta($attachment_id, 'sm_cloud', true); + $sm_cloud = apply_filters('wp_stateless_get_file', [], $attachment_id, true); // If metadata not passed the get metadata from post meta. if (empty($metadata)) { $metadata = get_post_meta($attachment_id, '_wp_attachment_metadata', true); @@ -1525,7 +1564,7 @@ public function wp_get_attachment_metadata($metadata, $attachment_id) { public function get_attached_file($file, $attachment_id) { global $default_dir; - $sm_cloud = get_post_meta($attachment_id, 'sm_cloud', true); + $sm_cloud = apply_filters('wp_stateless_get_file', [], $attachment_id); $_file = get_post_meta($attachment_id, '_wp_attached_file', true); /* Determine if the media file has GS data at all. */ @@ -1661,7 +1700,11 @@ public function run_install_process() { */ public function run_upgrade_process() { // Creating database on new installation. - $this->create_db(); + $this->create_sync_db(); + ud_stateless_db()->create_db(); + + Migrator::instance()->migrate(); + /** * Maybe Upgrade current Version */ @@ -1669,10 +1712,10 @@ public function run_upgrade_process() { } /** - * Create database on plugin activation. + * Create SYNC database on plugin activation. * @param boolean $force - whether to create db even if option exists. For debug purpose only. */ - public function create_db($force = false) { + public function create_sync_db($force = false) { global $wpdb; $sm_sync_db_version = get_option('sm_sync_db_version'); @@ -1703,13 +1746,10 @@ public function create_db($force = false) { * @param $old_site */ public function wp_delete_site($old_site) { - global $wpdb; - switch_to_blog($old_site->id); - $table_name = $wpdb->prefix . 'sm_sync'; - $sql = "DROP TABLE IF EXISTS $table_name"; - $wpdb->query($sql); + ud_stateless_db()->clear_db(); + restore_current_blog(); } @@ -1778,7 +1818,7 @@ public function show_notice_stateless_cache_busting() { */ public function wp_get_attachment_url($url = '', $post_id = '') { global $default_dir; - $sm_cloud = get_post_meta($post_id, 'sm_cloud', 1); + $sm_cloud = apply_filters('wp_stateless_get_file', [], $post_id); if (is_array($sm_cloud) && !empty($sm_cloud['fileLink'])) { $_url = parse_url($sm_cloud['fileLink']); $url = !isset($_url['scheme']) ? ('https:' . $sm_cloud['fileLink']) : $sm_cloud['fileLink']; @@ -1834,8 +1874,15 @@ public function attachment_url_to_postid($post_id, $url) { // User can use this constant if they change the Bucket Folder (root_dir) after uploading image. // This can be little slow at first run. if (empty($post_id)) { - $query = "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = 'sm_cloud' AND meta_value LIKE '%s'"; - $post_id = $wpdb->get_var($wpdb->prepare($query, '%' . $url . '%')); + if ( !defined('WP_STATELESS_POSTMETA') || !WP_STATELESS_POSTMETA ) { + $query = 'SELECT post_id FROM ' . ud_stateless_db()->files . ' WHERE file_link = %s'; + $post_id = $wpdb->get_var($wpdb->prepare($query, $url)); + } + + if (empty($post_id)) { + $query = "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = 'sm_cloud' AND meta_value LIKE '%s'"; + $post_id = $wpdb->get_var($wpdb->prepare($query, '%' . $url . '%')); + } if ($post_id) { set_transient("stateless_url_to_postid_" . md5($url), $post_id); @@ -1998,6 +2045,49 @@ function get_image_sizes($unset_disabled = true) { return $sizes; } + + /** + * Get the email for notifications + * + * @return string + */ + public function get_notification_email() { + $type = $this->get( 'sm.status_email_type' ); + + switch ($type) { + case 'true': + // Emails to Admin + return get_site_option('admin_email'); + case 'custom': + // Emails to Custom Email + return $this->get( 'sm.status_email_address' ); + } + + // Emails disabled + return ''; + } + + /** + * Send admin email + * + * @param string $subject + * @param string $message + * @param string $email + */ + public function send_admin_email($subject, $message, $email = '') { + $admin_email = empty($email) ? $this->get_notification_email() : $email; + + $admin_email = explode(',', $admin_email); + $admin_email = array_map( function($item) { + return strip_tags( trim($item) ); + }, $admin_email); + + if ( empty($admin_email) || empty($subject) || empty($message) ) { + return; + } + + wp_mail( $admin_email, $subject, $message); + } } } } diff --git a/lib/classes/class-db.php b/lib/classes/class-db.php new file mode 100644 index 000000000..d4dfa7746 --- /dev/null +++ b/lib/classes/class-db.php @@ -0,0 +1,897 @@ + 'id', + 'post_id' => 'post_id', + 'bucket' => 'bucket', + 'name' => 'name', + 'generation' => 'generation', + 'cacheControl' => 'cache_control', + 'contentType' => 'content_type', + 'contentDisposition' => 'content_disposition', + 'filesize' => 'file_size', + 'width' => 'width', + 'height' => 'height', + 'stateless_version' => 'stateless_version', + 'storageClass' => 'storage_class', + 'fileLink' => 'file_link', + 'selfLink' => 'self_link', + ]; + + /** + * Files sizes table fields mapping, used for backward compatibility with old postmeta + */ + private $file_sizes_mapping = [ + 'id' => 'id', + 'post_id' => 'post_id', + 'size_name' => 'size_name', + 'name' => 'name', + 'generation' => 'generation', + 'filesize' => 'file_size', + 'width' => 'width', + 'height' => 'height', + 'fileLink' => 'file_link', + 'selfLink' => 'self_link', + ]; + + protected function __construct() { + global $wpdb; + $this->wpdb = $wpdb; + + $this->files = $this->wpdb->prefix . 'stateless_files'; + $this->file_sizes = $this->wpdb->prefix . 'stateless_file_sizes'; + $this->file_meta = $this->wpdb->prefix . 'stateless_file_meta'; + $this->sm_sync = $this->wpdb->prefix . 'sm_sync'; + + $image_host = ud_get_stateless_media()->get_gs_host(); + $this->bucket_link = apply_filters('wp_stateless_bucket_link', $image_host); + + if ( is_multisite() ) { + $this->cache_group = implode('_', [ + $this->cache_group, + get_current_blog_id(), + ]); + } + + $this->_init(); + } + + /** + * Init hooks + */ + private function _init() { + add_filter('wp_stateless_generate_cloud_meta', [$this, 'process_cloud_meta'], 10, 5); + add_action('deleted_post', [$this, 'delete_post'], 10, 2); + + add_filter('wp_stateless_get_file', [$this, 'get_file'], 10, 3); + add_filter('wp_stateless_get_file_sizes', [$this, 'get_file_sizes'], 10, 2); + add_filter('wp_stateless_get_file_meta', [$this, 'get_file_meta'], 10, 2); + add_filter('wp_stateless_get_file_meta_value', [$this, 'get_file_meta_value'], 10, 4); + add_action('wp_stateless_set_file', [$this, 'set_file'], 10, 2); + add_action('wp_stateless_set_file_size', [$this, 'set_file_size'], 10, 3); + add_action('wp_stateless_set_file_meta', [$this, 'update_file_meta'], 10, 3); + } + + /** + * Getters + */ + public function __get($property) { + if ( in_array($property, ['files', 'file_sizes', 'file_meta', 'sm_sync']) ) { + return $this->$property; + } + } + + /** + * Creates or updates DB structure + */ + public function create_db() { + $version = get_site_option('sm_db_version'); + + if ($version === self::DB_VERSION) { + return; + } + + $charset_collate = $this->wpdb->get_charset_collate(); + + $sql = "CREATE TABLE $this->files ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `post_id` bigint(20) unsigned NULL DEFAULT NULL, + `bucket` varchar(255) NOT NULL, + `name` varchar(255) NOT NULL, + `generation` bigint(16) NOT NULL, + `cache_control` varchar(255) NULL DEFAULT NULL, + `content_type` varchar(255) NULL DEFAULT NULL, + `content_disposition` varchar(100) NULL DEFAULT NULL, + `file_size` bigint(20) unsigned NULL DEFAULT NULL, + `width` int unsigned NULL DEFAULT NULL, + `height` int unsigned NULL DEFAULT NULL, + `stateless_version` varchar(20) NOT NULL, + `storage_class` varchar(50) NULL DEFAULT NULL, + `file_link` text NOT NULL, + `self_link` text NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY post_id (post_id) + ) $charset_collate; + + CREATE TABLE $this->file_sizes ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `post_id` bigint(20) unsigned NULL DEFAULT NULL, + `size_name` varchar(255) NOT NULL, + `name` varchar(255) NOT NULL, + `generation` bigint(16) NOT NULL, + `file_size` bigint(20) unsigned NULL DEFAULT NULL, + `width` int unsigned NULL DEFAULT NULL, + `height` int unsigned NULL DEFAULT NULL, + `file_link` text NOT NULL, + `self_link` text NOT NULL, + PRIMARY KEY (`id`), + KEY post_id (post_id), + UNIQUE KEY post_id_size (post_id, size_name) + ) $charset_collate; + + CREATE TABLE $this->file_meta ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `post_id` bigint(20) unsigned NULL DEFAULT NULL, + `meta_key` varchar(255) NOT NULL, + `meta_value` longtext NOT NULL, + PRIMARY KEY (`id`), + KEY post_id (post_id), + UNIQUE KEY post_id_key (post_id, meta_key) + ) $charset_collate;"; + + require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); + dbDelta($sql); + + update_site_option('sm_db_version', self::DB_VERSION); + } + + /** + * Remove custom DB table on plugin uninstall + */ + public function clear_db() { + $sql = "DROP TABLE IF EXISTS $this->files, $this->file_sizes, $this->file_meta, $this->sm_sync;"; + $this->wpdb->query($sql); + } + + /** + * Get file link + * + * @param string $name + * @return string + */ + public function get_file_link($name) { + return trailingslashit($this->bucket_link) . $name; + } + + /** + * Get file row ID by post ID + * + * @param int $post_id + * @return int | null + */ + public function get_file_id($post_id) { + $sql = "SELECT id FROM $this->files WHERE post_id = %d"; + $id = $this->wpdb->get_var( $this->wpdb->prepare($sql, $post_id) ); + + return $id ? (int) $id : null; + } + + /** + * Get file size row ID by post ID and size name + * + * @param int $post_id + * @param string $size + * @return int | null + */ + public function get_file_size_id($post_id, $size) { + $sql = "SELECT id FROM $this->file_sizes WHERE post_id = %d AND size_name = %s"; + $id = $this->wpdb->get_var( $this->wpdb->prepare($sql, $post_id, $size) ); + + return $id ? (int) $id : null; + } + + /** + * Get file meta row ID by post ID and meta key + * + * @param int $post_id + * @param string $key + * @return int | null + */ + public function get_file_meta_id($post_id, $key) { + $key = sanitize_key($key); + + $sql = "SELECT id FROM $this->file_meta WHERE post_id = %d AND meta_key = %s"; + $id = $this->wpdb->get_var( $this->wpdb->prepare($sql, $post_id, $key) ); + + return $id ? (int) $id : null; + } + + /** + * Add file data to DB + * + * @param array $cloud_meta - generated WP postmeta + * @param array $media - GCS media object + * @param string $image_size - image size name + * @param array $img - image (or image size) data for upload + * @param string $bucketLink - bucket link + * @return mixed + */ + public function process_cloud_meta($cloud_meta, $media, $image_size, $img, $bucketLink) { + $this->update_data($media); + + return $cloud_meta; + } + + /** + * Determine which data to update and run updates + * + * @param array $media - GCS media object + */ + public function update_data($media) { + $error = false; + + if ( !isset($media['name']) || !isset($media['metadata']) || !isset($media['metadata']['size']) ) { + $error = 'Metadata missing or incorrect for GCS media object'; + } else { + $size = $media['metadata']['size']; + + if ( $size == self::FULL_SIZE ) { + $attachment_id = isset($media['metadata']['object-id']) ? $media['metadata']['object-id'] : null; + + if ( !$attachment_id ) { + $error = 'Unable to get attachment ID for GCS media object'; + } else { + $this->_update_file( + $attachment_id, + $this->_get_file_from_media($media), + ); + } + } else { + $attachment_id = isset($media['metadata']['child-of']) ? $media['metadata']['child-of'] : null; + + if ( !$attachment_id ) { + $error = 'Unable to get parent attachment ID for GCS media object'; + } else { + $this->_update_file_size( + $attachment_id, + $size, + $this->_get_file_size_from_media($media), + ); + } + } + } + + if ( $error ) { + Helper::log($error); + Helper::log($media, true); + } + } + + /** + * Update file data, based on the mapping received from the media object + * + * @param int $attachment_id + * @param array $data + * @return int | null + */ + public function set_file($attachment_id, $data) { + $result = []; + + foreach ($this->file_mapping as $key => $mapping) { + if ( isset($data[$key]) ) { + $result[$mapping] = $data[$key]; + + continue; + } + + if ( isset($data[$mapping]) ) { + $result[$mapping] = $data[$mapping]; + + continue; + } + } + + unset($result['id']); + + return $this->_update_file($attachment_id, $result); + } + + /** + * Update file size data + * + * @param int $attachment_id + * @param string $size_name + * @param array $data + * @return int | null + */ + public function set_file_size($attachment_id, $size_name, $data) { + $result = []; + + foreach ($this->file_sizes_mapping as $key => $mapping) { + if ( isset($data[$key]) ) { + $result[$mapping] = $data[$key]; + + continue; + } + + if ( isset($data[$mapping]) ) { + $result[$mapping] = $data[$mapping]; + + continue; + } + } + + unset($result['id']); + + return $this->_update_file_size($attachment_id, $size_name, $result); + } + + /** + * Convert GSC media object into stateless file data + * + * @param array $media + * @return array + */ + private function _get_file_from_media($media) { + $name = $media['name']; + + $data =[ + 'bucket' => $media['bucket'] ?? '', + 'name' => $name, + 'generation' => $media['generation'] ?? '', + 'cache_control' => $media['cacheControl'] ?? null, + 'content_type' => $media['contentType'] ?? null, + 'content_disposition' => $media['contentDisposition'] ?? null, + 'file_size' => $media['size'] ?? null, + 'width' => $media['metadata']['width'] ?? null, + 'height' => $media['metadata']['height'] ?? null, + 'stateless_version' => get_option('wp_sm_version', false), + 'storage_class' => $media['storageClass'] ?? null, + 'file_link' => $this->get_file_link($name), + 'self_link' => $media['selfLink'] ?? '', + ]; + + return $data; + } + + /** + * Update file data + * + * @param int $attachment_id + * @param array $data + * @return int | null + */ + private function _update_file($attachment_id, $data) { + $file_id = $this->get_file_id($attachment_id); + + if ( $file_id ) { + $this->wpdb->update( + $this->files, + $data, + ['id' => $file_id] + ); + } else { + $data['post_id'] = $attachment_id; + + $this->wpdb->insert( $this->files, $data ); + + $file_id = $this->wpdb->insert_id; + } + + $this->_delete_file_cache($attachment_id); + + return $file_id; + } + + /** + * Convert GSC media object into stateless file size data + * + * @param array $media + * @return array + */ + private function _get_file_size_from_media($media) { + $name = $media['name']; + + $data =[ + 'name' => $name, + 'generation' => $media['generation'] ?? '', + 'file_size' => $media['size'] ?? null, + 'width' => $media['metadata']['width'] ?? null, + 'height' => $media['metadata']['height'] ?? null, + 'file_link' => $this->get_file_link($name), + 'self_link' => $media['selfLink'] ?? '', + ]; + + return $data; + } + + /** + * Update file size data + * + * @param int $attachment_id + * @param string $size_name + * @param array $media + * @return int | null + */ + private function _update_file_size($attachment_id, $size_name, $data) { + $file_size_id = $this->get_file_size_id($attachment_id, $size_name); + + if ( $file_size_id ) { + $this->wpdb->update( + $this->file_sizes, + $data, + ['id' => $file_size_id] + ); + } else { + $data['post_id'] = $attachment_id; + $data['size_name'] = $size_name; + + $this->wpdb->insert( $this->file_sizes, $data ); + + $file_size_id = $this->wpdb->insert_id; + } + + $this->_delete_file_sizes_cache($attachment_id); + + return $file_size_id; + } + + /** + * Delete file data when attachment is deleted + * + * @param int $post_id + * @param \WP_Post $post + */ + public function delete_post($post_id, $post) { + $file_id = $this->get_file_id($post_id); + + if ( !$file_id ) { + return; + } + + $this->wpdb->delete( + $this->files, + ['post_id' => $post_id] + ); + + $this->wpdb->delete( + $this->file_sizes, + ['post_id' => $post_id] + ); + + $this->wpdb->delete( + $this->file_meta, + ['post_id' => $post_id] + ); + + $this->_delete_attachment_cache($post_id); + } + + /** + * Map requested fields into database query< compatible with the old post meta structure + * + * @param array $fields + * @param array $fields_mapping + * @return string + */ + private function _map_fields($fields, $fields_mapping) { + $mapped = []; + $result = []; + + if ( !is_array($fields) ) { + $fields = [$fields]; + } + + if (empty($fields)) { + $fields = array_keys($fields_mapping); + } + + foreach ($fields as $key) { + if ( isset($fields_mapping[$key]) ) { + $mapped[$key] = $fields_mapping[$key]; + } + } + + foreach ($mapped as $key => $value) { + $result[] = "$value AS $key"; + } + + return implode(', ', $result); + } + + /** + * Get cache key for file data + * + * @param int $post_id + * @return string + */ + private function _get_file_cache_key($post_id) { + return implode('_', ['file', $post_id]); + } + + /** + * Get cache key for file sizes data + * + * @param int $post_id + * @return string + */ + private function _get_file_sizes_cache_key($post_id) { + return implode('_', ['file_sizes', $post_id]); + } + + /** + * Get cache group for file meta data + * + * @param int $post_id + * @return string + */ + private function _get_file_meta_cache_group($post_id) { + return implode('_', [$this->cache_group, 'meta', $post_id]); + } + + /** + * Get cache key for file meta data + * + * @param int $post_id + * @param string $key + * @return string + */ + private function _get_file_meta_cache_key($post_id, $key) { + return implode('_', [$key, $post_id]); + } + + /** + * Get cache for file meta + * + * @param int $post_id + * @param string $key + * @param bool $found + * @return mixed + */ + private function _get_file_meta_cache($post_id, $key, &$found) { + return wp_cache_get( + $this->_get_file_meta_cache_key($post_id, $key), + $this->_get_file_meta_cache_group($post_id), + false, + $found + ); + } + + /** + * Set cache for file meta + * + * @param int $post_id + * @param string $key + * @param mixed $value + * @return bool + */ + private function _set_file_meta_cache($post_id, $key, $value) { + return wp_cache_set( + $this->_get_file_meta_cache_key($post_id, $key), + $value, + $this->_get_file_meta_cache_group($post_id) + ); + } + + /** + * Delete file cache + * + * @param int $attachment_id + */ + private function _delete_file_cache($attachment_id) { + wp_cache_delete( $this->_get_file_cache_key($attachment_id), $this->cache_group ); + } + + /** + * Delete file sizes cache + * + * @param int $attachment_id + */ + private function _delete_file_sizes_cache($attachment_id) { + wp_cache_delete( $this->_get_file_sizes_cache_key($attachment_id), $this->cache_group ); + } + + /** + * Delete file meta cache for single key + * + * @param int $attachment_id + * @param string $key + */ + private function _delete_file_meta_key_cache($attachment_id, $key) { + wp_cache_delete( + $this->_get_file_meta_cache_key($attachment_id, $key), + $this->_get_file_meta_cache_group($attachment_id) + ); + } + + /** + * Delete all file meta cache + * + * @param int $attachment_id + */ + private function _delete_file_meta_cache($attachment_id) { + wp_cache_flush_group( $this->_get_file_meta_cache_group($attachment_id) ); + } + + /** + * Delete all attachment cache + * + * @param int $attachment_id + */ + private function _delete_attachment_cache($attachment_id) { + $this->_delete_file_cache($attachment_id); + $this->_delete_file_sizes_cache($attachment_id); + $this->_delete_file_meta_cache($attachment_id); + } + + /** + * Get the total files count known to WP-Stateless + * + * @return int + */ + public function get_total_files() { + global $wpdb; + + try { + $query = "SELECT COUNT(post_id) FROM $this->files"; + + return $wpdb->get_var($query); + } catch (\Throwable $e) { + return 0; + } + } + + /** + * Get file data. If $with_sizes is set to true, all sizes will be included + * + * @param array $meta + * @param int $attachment_id + * @param bool $with_sizes + * @return array + */ + public function get_file($meta, $attachment_id, $with_sizes = false) { + if ( defined('WP_STATELESS_POSTMETA') && WP_STATELESS_POSTMETA ) { + $meta = get_post_meta($attachment_id, 'sm_cloud', true); + + if ( !empty($meta) ) { + return $meta; + } + } + + // Get values from the cache + $cache_key = $this->_get_file_cache_key($attachment_id); + + $meta = wp_cache_get($cache_key, $this->cache_group, false, $found); + + if ( $found ) { + return $meta; + } + + // Get values from the DB + $fields = $this->_map_fields([], $this->file_mapping); + + $sql = "SELECT $fields FROM $this->files WHERE post_id = %d"; + + $meta = $this->wpdb->get_row( $this->wpdb->prepare($sql, $attachment_id), ARRAY_A ); + + if ( empty($meta) ) { + return get_post_meta($attachment_id, 'sm_cloud', true); + } + + wp_cache_set($cache_key, $meta, $this->cache_group); + + // Get file size meta data + if ( $with_sizes ) { + $meta['sizes'] = apply_filters('wp_stateless_get_file_sizes', [], $attachment_id); + } + + return $meta; + } + + /** + * Get file sizes data + * + * @param array $sizes + * @param int $attachment_id + * @return array + */ + public function get_file_sizes($sizes, $attachment_id) { + if ( defined('WP_STATELESS_POSTMETA') && WP_STATELESS_POSTMETA ) { + $meta = get_post_meta($attachment_id, 'sm_cloud', true); + return isset($meta['sizes']) ? $meta['sizes'] : []; + } + + // Get values from the cache + $cache_key = $this->_get_file_sizes_cache_key($attachment_id); + + $sizes = wp_cache_get($cache_key, $this->cache_group, false, $found); + + if ( $found ) { + return $sizes; + } + + // Get values from the DB + $fields = $this->_map_fields([], $this->file_sizes_mapping); + + $sql = "SELECT $fields FROM $this->file_sizes WHERE post_id = %d"; + $sql = $this->wpdb->prepare($sql, $attachment_id); + + $result = $this->wpdb->get_results( $this->wpdb->prepare($sql, $attachment_id), ARRAY_A ); + $sizes = []; + + if ( !empty($result) ) { + foreach ($result as $size) { + $size_name = $size['size_name']; + unset($size['size_name']); + $sizes[$size_name] = $size; + } + } + + wp_cache_set($cache_key, $sizes, $this->cache_group); + + return $sizes; + } + + /** + * Get file meta + * + * @param int $post_id + * @param string $key + * @param mixed $default + * @return mixed + */ + public function get_file_meta_value($value, $post_id, $key, $default = null) { + if ( defined('WP_STATELESS_POSTMETA') && WP_STATELESS_POSTMETA ) { + $meta = get_post_meta($post_id, 'sm_cloud', []); + + return isset($meta[$key]) ? $meta[$key] : $default; + } + + // Get values from the cache + $key = sanitize_key($key); + + $value = $this->_get_file_meta_cache($post_id, $key, $found); + + if ( $found ) { + return $value; + } + + // Get values from the DB + $sql = "SELECT meta_value FROM $this->file_meta WHERE post_id = %d AND meta_key = %s"; + $data = $this->wpdb->get_var( $this->wpdb->prepare($sql, $post_id, $key) ); + + $value = null; + + if ( is_serialized( $data ) ) { // Don't attempt to unserialize data that wasn't serialized going in. + $value = @unserialize( trim( $data ) ); + } else { + $value = $data; + } + + $this->_set_file_meta_cache($post_id, $key, $value); + + return $value; + } + + /** + * Update file meta + * + * @param int $post_id + * @param string $key + * @param mixed $value + * @return int + */ + public function update_file_meta($post_id, $key, $value) { + $key = sanitize_key($key); + + // Update value in the DB + $value = maybe_serialize( $value ); + + $file_meta_id = $this->get_file_meta_id($post_id, $key); + + if ( $file_meta_id ) { + $this->wpdb->update( + $this->file_meta, + ['meta_value' => $value], + ['id' => $file_meta_id], + ); + } else { + $this->wpdb->insert( $this->file_meta, [ + 'meta_key' => $key, + 'meta_value' => $value, + 'post_id' => $post_id, + ]); + + $file_meta_id = $this->wpdb->insert_id; + } + + $this->_delete_file_meta_key_cache($post_id, $key); + + return $file_meta_id; + } + + /** + * Get all the meta for the file + * + * @param mixed $meta + * @param int $post_id + * @return mixed + */ + public function get_file_meta($meta, $post_id) { + $sql = "SELECT meta_key, meta_value FROM $this->file_meta WHERE post_id = %d"; + + $result = $this->wpdb->get_results( $this->wpdb->prepare($sql, $post_id), ARRAY_A ); + + $data = []; + + foreach ($result as $row) { + $key = $row['meta_key']; + $value = $row['meta_value']; + + if ( is_serialized( $value ) ) { // Don't attempt to unserialize data that wasn't serialized going in. + $value = @unserialize( trim( $value ) ); + } + + $data[$key] = $value; + } + + return $data; + } + } + } +} diff --git a/lib/classes/class-errors.php b/lib/classes/class-errors.php index b2b27026f..3e94b6095 100644 --- a/lib/classes/class-errors.php +++ b/lib/classes/class-errors.php @@ -180,35 +180,37 @@ public function admin_notices() { wp_enqueue_script( "ud-dismiss", $script_path, array( 'jquery' ) ); wp_localize_script( "ud-dismiss", "_ud_vars", array( "ajaxurl" => admin_url( 'admin-ajax.php' ), - ) ); + )); wp_localize_script( "sateless-error-notice-js", "stateless_error_notice_vars", array( "dismiss_nonce" => wp_create_nonce( 'stateless_notice_dismiss' ), "enable_action_nonce" => wp_create_nonce( 'stateless_enable_notice_button_action' ), - ) ); + )); //** Don't show the message if the user has no enough permissions. */ if ( ! function_exists( 'wp_get_current_user' ) ) { require_once( ABSPATH . 'wp-includes/pluggable.php' ); } - if( + $default_show = true; + + if ( empty( $this->args['type'] ) || ( $this->args['type'] == 'plugin' && !current_user_can( 'activate_plugins' ) ) || ( $this->args['type'] == 'theme' && !current_user_can( 'switch_themes' ) ) ) { - return; + $default_show = false; } //** Don't show the message if on a multisite and the user isn't a super user. */ if ( is_multisite() && ! is_super_admin() ) { - return; + $default_show = false; } $errors = apply_filters( 'ud:errors:admin_notices', $this->errors, $this->args ); $notices = apply_filters( 'stateless:notices:admin_notices', $this->notices, $this->args ); //** Errors Block */ - if( !empty( $errors ) && is_array( $errors ) ) { + if( $default_show && !empty( $errors ) && is_array( $errors ) ) { $message = ''; $data = array( 'title' => sprintf( __( '%s is not active due to following errors:', $this->domain ), esc_html($this->name) ), @@ -224,21 +226,47 @@ public function admin_notices() { //** Determine if warning has been dismissed */ if ( ! empty( $notices ) && is_array( $notices ) ) { //** Warnings Block */ - foreach($notices as $notice){ - if(get_option( 'dismissed_notice_' . $notice['key'] )){ + foreach ($notices as $notice ) { + if ( get_option( 'dismissed_notice_' . $notice['key'] )){ + continue; + } + + // Check additional capabilities + $capability = isset( $notice['capability'] ) && !empty( $notice['capability'] ) ? $notice['capability'] : null; + + if ( ( !$default_show && empty($capability) ) || ( !empty($capability) && !current_user_can($capability) ) ) { continue; } + // Additional HTML classes + $classes = []; + + if ( isset($notice['classes']) ) { + $classes = $notice['classes']; + + if ( !is_array($classes) ) { + $classes = [$classes]; + } + } + + $classes[] = 'notice'; + $data = wp_parse_args($notice, array( 'title' => '', - 'class' => 'notice', + 'class' => implode(' ', $classes), 'message' => '', 'button' => '', 'button_link' => '#', 'key' => '', 'action_links' => $this->action_links[ 'notices' ], + 'dismiss' => true, )); + $button_capability = isset( $notice['button_capability'] ) && !empty( $notice['button_capability'] ) ? $notice['button_capability'] : null; + if ( !empty($button_capability) && !current_user_can($button_capability) ) { + $data['button'] = ''; + } + include ud_get_stateless_media()->path( '/static/views/error-notice.php', 'dir' ); $has_notice = true; @@ -254,6 +282,10 @@ public function admin_notices() { public function dismiss_notices() { check_ajax_referer('stateless_notice_dismiss'); + if ( !is_user_logged_in() ) { + wp_send_json_error( array( 'error' => __( 'You are not allowed to do this action.', $this->domain ) ) ); + } + $response = array( 'success' => '0', 'error' => __( 'There was an error in request.', $this->domain ), @@ -269,6 +301,8 @@ public function dismiss_notices() { } if ( !$error && update_option( $option_key, time() ) ) { + do_action('wp_stateless_notice_dismissed', $option_key); + $response['success'] = '1'; $response['error'] = null; } @@ -283,9 +317,14 @@ public function dismiss_notices() { public function stateless_enable_notice_button_action(){ check_ajax_referer('stateless_enable_notice_button_action'); + if ( !is_user_logged_in() ) { + wp_send_json_error( array( 'error' => __( 'You are not allowed to do this action.', $this->domain ) ) ); + } + $response = array( 'success' => '1', ); + $error = false; if( empty($_POST['key']) ) { diff --git a/lib/classes/class-helper.php b/lib/classes/class-helper.php index 870f76c62..6e6f33852 100644 --- a/lib/classes/class-helper.php +++ b/lib/classes/class-helper.php @@ -64,5 +64,36 @@ public static function array_of_objects($array) { }, $array); } + /** + * Writes to error log. + * + * @param mixed $data + */ + public static function log($data, $json = false) { + if (!WP_DEBUG) { + return; + } + + if ( is_array($data) || is_object($data) || !is_string($data) ) { + if ( $json ) { + error_log( json_encode($data) ); + } else { + error_log( print_r($data, true) ); + } + + return; + } + + error_log($data); + } + + /** + * Writes debug to error log. + * + * @param mixed $data + */ + public static function debug($data, $json = false) { + self::log($data, $json); + } } } \ No newline at end of file diff --git a/lib/classes/class-migrator.php b/lib/classes/class-migrator.php new file mode 100644 index 000000000..4205124f7 --- /dev/null +++ b/lib/classes/class-migrator.php @@ -0,0 +1,371 @@ +path = ud_get_stateless_media()->path('static/migrations', 'dir'); + + $this->_init_hooks(); + } + + /** + * Initializes the needed hooks + */ + private function _init_hooks() { + add_action( 'init', [$this, 'show_messages'] ); + add_action( 'wp_stateless_batch_task_started', [$this, 'migration_started'], 10, 2 ); + add_action( 'wp_stateless_batch_task_failed', [$this, 'migration_failed'], 10, 3 ); + add_action( 'wp_stateless_batch_task_finished', [$this, 'migration_finished'], 10, 1 ); + add_filter( 'wp_stateless_batch_action_start', [$this, 'start_migration'], 10, 2); + add_action( 'wp_stateless_notice_dismissed', [$this, 'notice_dismissed'], 10, 1 ); + } + + /** + * Get migration ID from file name + * + * @param string $file + * @return string + */ + private function _file_to_id($file) { + return pathinfo($file, PATHINFO_FILENAME); + } + + /** + * Get migration ID from class name + * + * @param string $class + * @return string + */ + private function _class_to_id($class) { + return str_replace('Migration_', '', $class); + } + + /** + * Get migration class name from ID + * + * @param string $id + * @return string + */ + private function _id_to_class($id) { + return "\Migration_$id"; + } + + /** + * Get migration file name from ID + * + * @param string $id + * @return string + */ + private function _id_to_file($id) { + return "$this->path/$id.php"; + } + + /** + * Compares the list of files in the migrations directory with the list of finished migrations + * Finds the oldest migration that has not been run yet + * + * @return array + */ + private function _get_migration_ids() { + if ( !is_dir($this->path) ) { + return []; + } + + $ids = []; + $files = scandir($this->path, SCANDIR_SORT_ASCENDING); + + foreach ($files as $file) { + $extension = pathinfo($file, PATHINFO_EXTENSION); + + if ( $extension !== 'php' ) { + continue; + } + + $ids[] = $this->_file_to_id($file); + } + + return $ids; + } + + /** + * Returns the migration object + * + * @param string $id + * @return wpCloud\StatelessMedia\Batch\Migration + * @throws \Exception + */ + private function _get_object($id) { + $class = $this->_id_to_class($id); + + if ( !class_exists($class) ) { + require_once $this->_id_to_file($id); + } + + $object = new $class(); + + if ( !is_a($object, '\wpCloud\StatelessMedia\Batch\Migration') ) { + throw new \Exception("$class is not a valid migration"); + } + + $object->init_state(); + + return $object; + } + + /** + * Checks if any migrations required and sets or removes global flag + * + * @param array $migrations|null + */ + private function _check_required_migrations($migrations = null) { + if ( empty($migrations) ) { + $migrations = get_site_option(self::MIGRATIONS_KEY, []); + } + + $require_migrations = false; + + foreach ($migrations as $id => $migration) { + if ( !in_array( $migration['status'], [self::STATUS_FINISHED, self::STATUS_SKIPPED] ) ) { + $require_migrations = true; + break; + } + } + + if ( $require_migrations ) { + update_site_option(self::MIGRATIONS_NOTIFY_KEY, self::NOTIFY_REQUIRE); + delete_site_option(self::MIGRATIONS_NOTIFY_DISMISSED_KEY); + } else { + $notify = get_site_option(self::MIGRATIONS_NOTIFY_KEY, false); + + empty($notify) ? delete_site_option(self::MIGRATIONS_NOTIFY_KEY) : update_site_option(self::MIGRATIONS_NOTIFY_KEY, self::NOTIFY_FINISHED); + } + } + + /** + * Dismisses the migration notice + * + * @param string $option_name + */ + public function notice_dismissed($option_name) { + delete_site_option(self::MIGRATIONS_NOTIFY_KEY); + } + + /** + * Generates an updated list of migrations. + * Checks which migrations should run. + * Sets global options to display the requirement to run migrations. + * + * Is called by Bootstrap object during version upgrade on 'plugins_loaded' hook. + */ + public function migrate() { + // Rebuild the migrations list and state according to the new version + $ids = $this->_get_migration_ids(); + + $migrations = get_site_option(self::MIGRATIONS_KEY, []); + $existing = array_keys($migrations); + + foreach ($ids as $id) { + if ( in_array($id, $existing) ) { + continue; + } + + try { + $object = $this->_get_object($id); + $skip = !$object->should_run(); + + $migrations[$id] = [ + 'description' => $object->get_description(), + 'started' => '', + 'finished' => '', + 'status' => $object->should_run() ? self::STATUS_PENDING : self::STATUS_SKIPPED, + 'message' => '', + ]; + + } catch (\Throwable $e) { + Helper::log("Unable to initialize migration $id: " . $e->getMessage()); + } + } + + krsort($migrations); + + update_site_option(self::MIGRATIONS_KEY, $migrations); + + // Check if we need to run any migrations + $this->_check_required_migrations($migrations); + } + + /** + * Outputs the message that migrations are required + */ + public function show_messages() { + $is_running = BatchTaskManager::instance()->is_processing() || BatchTaskManager::instance()->is_paused(); + $notify = get_site_option(self::MIGRATIONS_NOTIFY_KEY, false); + + if ( $notify ) { + ud_get_stateless_media()->errors->add([ + 'title' => __('WP-Stateless: Data Optimization Required', ud_get_stateless_media()->domain), + 'message' => __('WP-Stateless has been updated! Your WP-Stateless data must now be optimized. Please backup your database before proceeding with the optimization.', ud_get_stateless_media()->domain), + 'button' => __('Optimize Data', ud_get_stateless_media()->domain), + 'button_link' => admin_url('upload.php?page=stateless-settings&tab=stless_status_tab'), + 'key' => 'migrations-required', + 'dismiss' => false, + 'classes' => ($notify == self::NOTIFY_REQUIRE) && !$is_running ? '' : 'hidden', + ], 'warning'); + + ud_get_stateless_media()->errors->add([ + 'title' => __('WP-Stateless: Data Optimization in Progress', ud_get_stateless_media()->domain), + 'message' => __('A background process is optimizing your WP-Stateless data. Please do not upload, change, or delete your media while this update is underway.', ud_get_stateless_media()->domain), + 'button' => __('View Progress', ud_get_stateless_media()->domain), + 'button_link' => admin_url('upload.php?page=stateless-settings&tab=stless_status_tab'), + 'key' => 'migrations-running', + 'dismiss' => false, + 'classes' => $is_running ? '' : 'hidden', + 'capability' => 'upload_files', + 'button_capability' => 'manage_options', + ], 'warning'); + + ud_get_stateless_media()->errors->add([ + 'title' => __('WP-Stateless: Data Optimization Complete', ud_get_stateless_media()->domain), + 'message' => __('Your WP-Stateless data has been optimized. You can now continue using your media as usual.', ud_get_stateless_media()->domain), + 'key' => 'migrations-finished', + 'classes' => ($notify == self::NOTIFY_FINISHED) && !$is_running ? '' : 'hidden', + ], 'warning'); + } + } + + /** + * Mark migration as started + * + * @param string $class + * @param string $file + */ + public function migration_started($class, $file) { + $migrations = get_site_option(self::MIGRATIONS_KEY, []); + $id = $this->_file_to_id($file); + + if ( array_key_exists($id, $migrations) ) { + $migrations[$id]['status'] = self::STATUS_RUNNING; + $migrations[$id]['started'] = time(); + + update_site_option(self::MIGRATIONS_KEY, $migrations); + } + } + + /** + * Mark migration as failed and check other migrations + * + * @param string $class + * @param string $file + * @param string $message + */ + public function migration_failed($class, $file, $message) { + $migrations = get_site_option(self::MIGRATIONS_KEY, []); + $id = $this->_file_to_id($file); + + if ( array_key_exists($id, $migrations) ) { + $migrations[$id]['status'] = self::STATUS_FAILED; + $migrations[$id]['message'] = $message; + + update_site_option(self::MIGRATIONS_KEY, $migrations); + $this->_check_required_migrations($migrations); + } + } + + /** + * Mark migration as completed and check other migrations + * + * @param string $class + */ + public function migration_finished($class) { + $migrations = get_site_option(self::MIGRATIONS_KEY, []); + $id = $this->_class_to_id($class); + + if ( array_key_exists($id, $migrations) ) { + $migrations[$id]['status'] = self::STATUS_FINISHED; + $migrations[$id]['finished'] = time(); + + update_site_option(self::MIGRATIONS_KEY, $migrations); + $this->_check_required_migrations($migrations); + } + } + + /** + * Run migration + * + * @param array $state + * @param array $params + * @return array + * @throws \Exception + */ + public function start_migration($state, $params) { + // Possibly not migration action + if ( empty($params['is_migration']) || empty($params['id']) || !$params['is_migration'] ) { + return $state; + } + + $id = $params['id']; + $migrations = get_site_option(self::MIGRATIONS_KEY, []); + + // Unknown migration? + if ( !array_key_exists($id, $migrations) ) { + return $state; + } + + $class = $this->_id_to_class($id); + $file = $this->_id_to_file($id); + + // Still possibly not migration action + if ( !file_exists($file) ) { + return $state; + } + + if ( $migrations[$id]['status'] !== self::STATUS_PENDING && !isset($params['force']) ) { + Helper::log("Migration $id is already started or finished. Status: " . $migrations[$id]['status']); + + return $state; + } + + if ( BatchTaskManager::instance()->is_running() ) { + Helper::log('Another batch task is already running'); + + return $state; + } + + $email = $params['email'] ?? ''; + + BatchTaskManager::instance()->start_task($class, $file, $email); + + return apply_filters('wp_stateless_batch_state', $state, []); + } +} diff --git a/lib/classes/class-module.php b/lib/classes/class-module.php index ca14f67c7..c7f8b9e84 100644 --- a/lib/classes/class-module.php +++ b/lib/classes/class-module.php @@ -25,11 +25,6 @@ public function __construct() { add_filter('wp_stateless_compatibility_tab_visible', array($this, 'compatibility_tab_visible'), 10, 1); add_action('wp_stateless_compatibility_tab_content', array($this, 'tab_content')); - /** - * Support for BuddyBoss - */ - new BuddyBoss(); - /** * Support for BuddyPress */ @@ -45,11 +40,6 @@ public function __construct() { */ new EDDDownloadMethod(); - /** - * Support for Elementor - */ - new Elementor(); - /** * Support for Ewww Image Optimizer */ diff --git a/lib/classes/class-settings.php b/lib/classes/class-settings.php index 145a2dc35..cd11a4ab5 100644 --- a/lib/classes/class-settings.php +++ b/lib/classes/class-settings.php @@ -46,6 +46,8 @@ final class Settings extends \UDX\Settings { 'organize_media' => array('', 'true'), 'hashify_file_name' => array(['WP_STATELESS_MEDIA_HASH_FILENAME' => 'WP_STATELESS_MEDIA_CACHE_BUSTING'], 'false'), 'dynamic_image_support' => array(['WP_STATELESS_MEDIA_ON_FLY' => 'WP_STATELESS_DYNAMIC_IMAGE_SUPPORT'], 'false'), + 'status_email_type' => array('', 'true'), + 'status_email_address' => array('', ''), ); private $network_only_settings = array( diff --git a/lib/classes/class-status.php b/lib/classes/class-status.php new file mode 100644 index 000000000..64f93c038 --- /dev/null +++ b/lib/classes/class-status.php @@ -0,0 +1,21 @@ +table_name = $wpdb->prefix . self::table; - ud_get_stateless_media()->create_db(); + ud_get_stateless_media()->create_sync_db(); // Manual sync using sync tab. // Return files to be manually sync from sync tab. diff --git a/lib/classes/class-utility.php b/lib/classes/class-utility.php index b5d596967..c95089bc6 100644 --- a/lib/classes/class-utility.php +++ b/lib/classes/class-utility.php @@ -254,7 +254,7 @@ public static function add_media($metadata, $attachment_id, $force = false, $arg } } - $cloud_meta = get_post_meta($attachment_id, 'sm_cloud', true); + $cloud_meta = apply_filters('wp_stateless_get_file', [], $attachment_id, true); $cloud_meta = wp_parse_args($cloud_meta, array( 'name' => '', @@ -920,11 +920,15 @@ public static function convert_to_byte($size) { public static function get_stateless_media_data_count() { global $wpdb; + if ( !defined('WP_STATELESS_POSTMETA') || !WP_STATELESS_POSTMETA ) { + return ud_stateless_db()->get_total_files(); + } + $stateless_media = $wpdb->get_var($wpdb->prepare(" - SELECT COUNT(meta_id) - FROM " . $wpdb->postmeta . " - WHERE meta_key = %s - ", 'sm_cloud')); + SELECT COUNT(meta_id) + FROM " . $wpdb->postmeta . " + WHERE meta_key = %s + ", 'sm_cloud')); return $stateless_media; } diff --git a/lib/classes/compatibility/buddyboss.php b/lib/classes/compatibility/buddyboss.php deleted file mode 100644 index 77e01fb67..000000000 --- a/lib/classes/compatibility/buddyboss.php +++ /dev/null @@ -1,50 +0,0 @@ -get('sm.mode'), ['disabled', 'backup'])) { - $url = ud_get_stateless_media()->get_gs_host() . '/' . $name; - } - } - } - } catch (\Exception $e) { - // @todo maybe log the exception. - } - // We are in filter so need to return the passed value. - return $url; - } - - /** - * To regenerate/delete files click Regenerate Files in - * Elementor >> Tools >> General >> Regenerate CSS - * All files will be deleted from GCS. - * And will be copied to GCS again on next page view. - */ - public function delete_elementor_files() { - do_action('sm:sync::deleteFiles', 'elementor/'); - } - - /** - * Delete GCS file on update/delete post. - * @param $post_ID - * @param null $post - * @param null $update - */ - public function delete_css_files($post_ID, $post = null, $update = null) { - if ($update || current_action() === 'deleted_post') { - $post_css = new \Elementor\Core\Files\CSS\Post($post_ID); - - // elementor/ css/ 'post-' . $post_id . '.css' - $name = $post_css::UPLOADS_DIR . $post_css::DEFAULT_FILES_DIR . $post_css->get_file_name(); - $name = apply_filters('wp_stateless_file_name', $name, 0); - do_action('sm:sync::deleteFile', $name); - } - } - - /** - * Delete elementor global css file when global style is updated on Elementor Editor. - * @param $success_response_data - * @param $id - * @param $data - * @return mixed - */ - public function delete_global_css($success_response_data, $id, $data) { - try { - $post_css = new \Elementor\Core\Files\CSS\Global_CSS('global.css'); - // elementor/ css/ 'global.css' - $name = $post_css::UPLOADS_DIR . $post_css::DEFAULT_FILES_DIR . $post_css->get_file_name(); - $name = apply_filters('wp_stateless_file_name', $name, 0); - do_action('sm:sync::deleteFile', $name); - } catch (\Exception $e) { - // @todo maybe log the exception. - } - // We are in filter so need to return the passed value. - return $success_response_data; - } - - /** - * @param $name - * @param $absolutePath - */ - public function filter_css_file($name, $absolutePath) { - if ($upload_data = wp_upload_dir() && file_exists($absolutePath)) { - try { - $content = file_get_contents($absolutePath); - - if (!empty($upload_data['baseurl']) && !empty($content)) { - $baseurl = preg_replace('/https?:\/\//', '', $upload_data['baseurl']); - $root_dir = trim(ud_get_stateless_media()->get('sm.root_dir'), '/ '); // Remove any forward slash and empty space. - $root_dir = apply_filters("wp_stateless_handle_root_dir", $root_dir); - $root_dir = !empty($root_dir) ? $root_dir . '/' : ''; - $image_host = ud_get_stateless_media()->get_gs_host() . $root_dir; - $file_ext = ud_get_stateless_media()->replaceable_file_types(); - - preg_match_all('/(https?:\/\/' . str_replace('/', '\/', $baseurl) . ')\/(.+?)(' . $file_ext . ')/i', $content, $matches); - if (!empty($matches)) { - foreach ($matches[0] as $key => $match) { - $id = attachment_url_to_postid($match); - if (!empty($id)) { - Utility::add_media(null, $id, true); - } - } - } - - $content = preg_replace('/(https?:\/\/' . str_replace('/', '\/', $baseurl) . ')\/(.+?)(' . $file_ext . ')/i', $image_host . '/$2$3', $content); - file_put_contents($absolutePath, $content); - preg_match('/post-(\d+).css/', $name, $match); - - if (!empty($match[1])) { - $_elementor_css = get_post_meta($match[1], '_elementor_css', true); - if (!empty($_elementor_css)) { - $_elementor_css['time'] = time(); - } - } - } - } catch (\Exception $e) { - } - } - } - } - } -} diff --git a/lib/classes/compatibility/lite-speed-cache.php b/lib/classes/compatibility/lite-speed-cache.php index e7ba5b6da..ebddc750f 100644 --- a/lib/classes/compatibility/lite-speed-cache.php +++ b/lib/classes/compatibility/lite-speed-cache.php @@ -54,6 +54,49 @@ public function module_init($sm) { add_action('sm:pre::synced::image', array($this, 'update_md5_and_manual_sync'), 10, 1); } + /** + * Get the md5_file hash from cloud meta. + * + * @param int $attachment_id + * @return array + */ + private function _get_md5_meta($attachment_id) { + // If WP-Stateless version >= 4.0.0 + if ( function_exists('ud_stateless_db') ) { + $cloud_meta = [ + 'fileMd5' => apply_filters('wp_stateless_get_file_meta_value', [], $attachment_id, 'fileMd5', []), + ]; + } else { + $cloud_meta = get_post_meta($attachment_id, 'sm_cloud', true); + } + + return $cloud_meta; + } + + /** + * Update the md5_file hash in cloud meta. + * + * @param int $attachment_id + * @param array $cloud_meta + */ + private function _update_md5_meta($attachment_id, $cloud_meta) { + // If WP-Stateless version >= 4.0.0, postmeta can be limited to 'fileMd5' only + // and we should not override it + $test_meta = $cloud_meta; + unset($test_meta['fileMd5']); + + if ( empty($test_meta) ) { + $test_meta = get_post_meta($attachment_id, 'sm_cloud', true); + $test_meta['fileMd5'] = $cloud_meta['fileMd5']; + $cloud_meta = $test_meta; + } + + update_post_meta($attachment_id, 'sm_cloud', $cloud_meta); + + // If WP-Stateless version >= 4.0.0 + do_action('wp_stateless_set_file_meta', $attachment_id, 'fileMd5', $cloud_meta['fileMd5']); + } + /** * Sync the image when Lite Speed plugin pull the optimized image. * We need to overwrite the existing image. @@ -89,7 +132,7 @@ public function module_init($sm) { public function sync_image($row_img, $local_file) { $rm_ori_bkup = apply_filters('litespeed_conf', 'img_optm-rm_bkup'); $gs_name = apply_filters('wp_stateless_file_name', $row_img->src); - $cloud_meta = get_post_meta($row_img->post_id, 'sm_cloud', true); + $cloud_meta = $this->_get_md5_meta($row_img->post_id); if (empty($cloud_meta)) $cloud_meta = array(); @@ -101,7 +144,7 @@ public function sync_image($row_img, $local_file) { } $cloud_meta['fileMd5'][$gs_name] = md5_file($local_file); - update_post_meta($row_img->post_id, 'sm_cloud', $cloud_meta); + $this->_update_md5_meta($row_img->post_id, $cloud_meta); do_action('sm:sync::syncFile', $gs_name, $local_file, 2); } @@ -116,9 +159,9 @@ public function sync_webp($row_img, $local_file) { if ($optm_webp) { $gs_name = apply_filters('wp_stateless_file_name', $row_img->src . '.webp'); - $cloud_meta = get_post_meta($row_img->post_id, 'sm_cloud', true); + $cloud_meta = $this->_get_md5_meta($row_img->post_id); $cloud_meta['fileMd5'][$gs_name] = md5_file($local_file); - update_post_meta($row_img->post_id, 'sm_cloud', $cloud_meta); + $this->_update_md5_meta($row_img->post_id, $cloud_meta); add_filter('upload_mimes', array($this, 'add_webp_mime'), 10, 2); do_action('sm:sync::syncFile', $gs_name, $local_file, 2, array('use_root' => true)); @@ -157,7 +200,7 @@ public function litespeed_media_info($info, $short_file_path, $post_id) { try { $metadata = wp_get_attachment_metadata($post_id); - $cloud_meta = get_post_meta($post_id, 'sm_cloud', true); + $cloud_meta = $this->_get_md5_meta($post_id); if (!empty($metadata['gs_link'])) { $short_file_path = apply_filters('wp_stateless_file_name', $short_file_path); @@ -239,7 +282,7 @@ public function add_webp_mime($t, $user) { */ public function update_hash($attachment_id, $gs_name_new, $gs_name_old, $delete = false) { try { - $cloud_meta = get_post_meta($attachment_id, 'sm_cloud', true); + $cloud_meta = $this->_get_md5_meta($attachment_id); if (!$delete) { if ($gs_name_old && !empty($cloud_meta['fileMd5'][$gs_name_old])) { @@ -251,7 +294,9 @@ public function update_hash($attachment_id, $gs_name_new, $gs_name_old, $delete } if (isset($cloud_meta['fileMd5'][$gs_name_old])) unset($cloud_meta['fileMd5'][$gs_name_old]); - update_post_meta($attachment_id, 'sm_cloud', $cloud_meta); + + $this->_update_md5_meta($attachment_id, $cloud_meta); + return true; } catch (\Throwable $th) { error_log(print_r($th, true)); @@ -273,11 +318,27 @@ public function update_hash($attachment_id, $gs_name_new, $gs_name_old, $delete public function cloud_meta_add_file_md5($cloud_meta, $media, $image_size, $img, $bucketLink) { if ($file_hash = md5_file($img['path'])) { $gs_name = !empty($media['name']) ? $media['name'] : $img['gs_name']; - $extension = pathinfo($gs_name, PATHINFO_EXTENSION); - $bk_file = substr($gs_name, 0, -strlen($extension)) . 'bk.' . $extension; // Storing file hash $cloud_meta['fileMd5'][$gs_name] = $file_hash; + + // If WP-Stateless version >= 4.0.0 + if ( function_exists('ud_stateless_db') ) { + $metadata = $media['metadata'] ?? []; + + $post_id = null; + + if ( isset($metadata['object-id']) ) { + $post_id = $metadata['object-id']; + } else if ( isset($metadata['child-of']) ) { + $post_id = $metadata['child-of']; + } + + $meta = apply_filters('wp_stateless_get_file_meta_value', [], $post_id, 'fileMd5', []); + $meta[$gs_name] = $file_hash; + + do_action('wp_stateless_set_file_meta', $post_id, 'fileMd5', $meta); + } } return $cloud_meta; @@ -290,7 +351,7 @@ public function cloud_meta_add_file_md5($cloud_meta, $media, $image_size, $img, * @param $metadata */ public function manual_sync_backup_file($attachment_id, $metadata) { - $cloud_meta = get_post_meta($attachment_id, 'sm_cloud', true); + $cloud_meta = $this->_get_md5_meta($attachment_id); if (!empty($cloud_meta['fileMd5'])) { $upload_dir = wp_upload_dir(); @@ -317,7 +378,8 @@ public function manual_sync_backup_file($attachment_id, $metadata) { * @param $attachment_id */ public function update_md5_and_manual_sync($attachment_id) { - $cloud_meta = get_post_meta($attachment_id, 'sm_cloud', true); + $cloud_meta = $this->_get_md5_meta($attachment_id); + $metadata = wp_get_attachment_metadata($attachment_id); $image_sizes = Utility::get_path_and_url($metadata, $attachment_id); @@ -370,7 +432,7 @@ public function update_md5_and_manual_sync($attachment_id) { } } - update_post_meta($attachment_id, 'sm_cloud', $cloud_meta); + $this->_update_md5_meta($attachment_id, $cloud_meta); } } } diff --git a/lib/classes/compatibility/polylang-pro.php b/lib/classes/compatibility/polylang-pro.php index c24598517..7907d3092 100644 --- a/lib/classes/compatibility/polylang-pro.php +++ b/lib/classes/compatibility/polylang-pro.php @@ -42,7 +42,8 @@ private function get_stateless_meta($post_id) { // Get other attachment ids for the same file $ids = pll_get_post_translations($post_id); - + + // Search for the first attachment with WP Stateless meta foreach ($ids as $id) { $meta = get_post_meta($id, 'sm_cloud', true); @@ -55,6 +56,54 @@ private function get_stateless_meta($post_id) { return $metadata; } + private function get_stateless_data($post_id) { + // In case Polylang is not active of codebase not compatible anymore + if (!function_exists('pll_get_post_translations')) { + return null; + } + + // For the compatibility with the older versions of WP Stateless + if ( !function_exists('ud_stateless_db') ) { + return null; + } + + $ids = pll_get_post_translations($post_id); + + $data = []; + + foreach ($ids as $id) { + $file = apply_filters( 'wp_stateless_get_file', [], $id ); + + if ( !empty($file) && !empty($file['name']) ) { + $data['file'] = $file; + + break; + } + } + + foreach ($ids as $id) { + $sizes = apply_filters( 'wp_stateless_get_file_sizes', [], $id ); + + if ( !empty($sizes) ) { + $data['sizes'] = $sizes; + + break; + } + } + + foreach ($ids as $id) { + $meta = apply_filters( 'wp_stateless_get_file_meta', [], $id ); + + if ( !empty($meta) ) { + $data['meta'] = $meta; + + break; + } + } + + return $data; + } + /** * @param $post_id * @param $tr_id @@ -77,6 +126,7 @@ public function pll_translate_media($post_id, $tr_id, $lang_slug) { } } + // Duplicate WP Stateless meta for the new attachment $cloud_meta = $this->get_stateless_meta($attachment_id); if ( !empty($cloud_meta) ) { @@ -86,6 +136,31 @@ public function pll_translate_media($post_id, $tr_id, $lang_slug) { update_post_meta($tr_id, 'sm_cloud', wp_slash($cloud_meta)); } + // Duplicate WP Stateless data for the new attachment and sizes + $data = $this->get_stateless_data($attachment_id); + + if ( !empty($data) ) { + try { + foreach ( [$attachment_id, $tr_id] as $id ) { + do_action('wp_stateless_set_file', $id, $data['file']); + + $sizes = $data['sizes'] ?? []; + + foreach ($sizes as $name => $size) { + do_action('wp_stateless_set_file_size', $id, $name, $size); + } + + $meta = $data['meta'] ?? []; + + foreach ($meta as $key => $value) { + do_action('wp_stateless_set_file_meta', $id, $key, $value); + } + } + } catch (\Throwable $e) { + error_log( $e->getMessage() ); + } + } + return $metadata; }, 10, 4); } diff --git a/lib/classes/status/class-info.php b/lib/classes/status/class-info.php new file mode 100644 index 000000000..43c7199af --- /dev/null +++ b/lib/classes/status/class-info.php @@ -0,0 +1,85 @@ +_init_hooks(); + } + + private function _init_hooks() { + add_action('wp_stateless_status_tab_content', [$this, 'tab_content'], 10); + } + + /** + * Get the total attachments count + * + * @return int + */ + private function _get_total_attachments() { + global $wpdb; + + try { + $query = "SELECT COUNT(*) " . + "FROM $wpdb->posts " . + "WHERE post_type = 'attachment' AND post_status != 'trash'"; + + return $wpdb->get_var($query); + } catch (\Throwable $e) { + return 0; + } + + return wp_count_attachments(); + } + + /** + * Outputs 'Health Status' tab content on the settings page. + */ + public function tab_content() { + $rows = [ + [ + 'label' => __('Total Attachments in Wordpress', ud_get_stateless_media()->domain), + 'value' => $this->_get_total_attachments(), + ], + [ + 'label' => __('Total Attachments in WP-Stateless', ud_get_stateless_media()->domain), + 'value' => ud_stateless_db()->get_total_files(), + ], + ]; + + $rows = Helper::array_of_objects($rows); + + include ud_get_stateless_media()->path('static/views/status-sections/info.php', 'dir'); + } +} diff --git a/lib/classes/status/class-migrations.php b/lib/classes/status/class-migrations.php new file mode 100644 index 000000000..10dce1426 --- /dev/null +++ b/lib/classes/status/class-migrations.php @@ -0,0 +1,245 @@ +_init_hooks(); + } + + private function _init_hooks() { + add_action('wp_stateless_status_tab_content', [$this, 'tab_content'], 50); + add_filter('wp_stateless_batch_state', [$this, 'migrations_state'], 20, 2); + } + + /** + * Detects the oldest migration that needs to be finished before the next one can run + */ + private function _set_primary_migration_id() { + $this->primary_migration_id = ''; + + $migrations = array_reverse($this->migrations, true); + + foreach ($migrations as $id => $migration) { + if ( in_array($migration['status'], [Migrator::STATUS_PENDING, Migrator::STATUS_RUNNING, Migrator::STATUS_FAILED]) ) { + $this->primary_migration_id = $id; + break; + } + } + } + + /** + * Returns the id of the migration that is currently running (if any) + * + * @param array $state + * @return string|bool|null + */ + private function _set_running_migration_id($state) { + $this->running_migration_id = ''; + + if ( !empty($state) ) { + if ( isset($state['is_migration']) && $state['is_migration'] ) { + $this->running_migration_id = $state['id']; // migration is running + + return; + } + } + } + + /** + * Generate the message for the migration, depending on the current state + * + * @param string $id + * @param array $migration + * @return string + */ + private function _get_migration_message($id, $migration) { + $message = $migration['message'] ?? ''; + + switch ($migration['status']) { + case Migrator::STATUS_FINISHED: + $format = get_option('date_format') . ' ' . get_option( 'time_format' ); + $date = wp_date($format, $migration['finished']); + $message = sprintf( __('Finished at %s', ud_get_stateless_media()->domain), $date ); + break; + case Migrator::STATUS_SKIPPED: + $message = !empty($message) ? $message : __('Not required', ud_get_stateless_media()->domain); + break; + case Migrator::STATUS_FAILED: + $message = sprintf( __('Failed to finish: %s', ud_get_stateless_media()->domain), $message ); + break; + case Migrator::STATUS_PENDING: + if ( $id == $this->primary_migration_id ) { + $message = sprintf( __('Ready to run', ud_get_stateless_media()->domain) ); + } else { + $message = sprintf( + __('Requires %s to finish', ud_get_stateless_media()->domain), + $this->migrations[$this->primary_migration_id]['description'] + ); + } + break; + case Migrator::STATUS_RUNNING: + $message = __('In progress...', ud_get_stateless_media()->domain); + break; + } + + return $message; + } + + /** + * Returns the status of the migration + * + * @param string $status + * @return string + */ + public static function get_status_text($status) { + switch ($status) { + case Migrator::STATUS_PENDING: + return __('Pending', ud_get_stateless_media()->domain); + case Migrator::STATUS_RUNNING: + return __('Running', ud_get_stateless_media()->domain); + case Migrator::STATUS_SKIPPED: + return __('Skipped', ud_get_stateless_media()->domain); + case Migrator::STATUS_FINISHED: + return __('Finished', ud_get_stateless_media()->domain); + case Migrator::STATUS_FAILED: + return __('Failed', ud_get_stateless_media()->domain); + } + } + + /** + * Get migrations state for frontend display + * + * @return array + */ + private function _get_migrations_state($state = null) { + $migrations = []; + $this->migrations = get_site_option(Migrator::MIGRATIONS_KEY, []); + + if ( !is_array($this->migrations) ) { + $this->migrations = []; + } + + $defaults = [ + 'description' => '', + 'status' => '', + 'status_text' => '', + 'message' => '', + 'can_start' => false, + 'can_pause' => false, + 'can_resume' => false, + ]; + + $this->_set_primary_migration_id(); + $batch_state = $state ? $state : apply_filters('wp_stateless_batch_state', [], []); + $this->_set_running_migration_id($batch_state); + + foreach ($this->migrations as $id => $migration) { + $status = $migration['status']; + $can_start = $this->primary_migration_id == $id; + $can_pause = false; + $can_resume = false; + + if ( $this->running_migration_id == $id ) { + $can_start = false; + + if ( $batch_state['is_paused'] ) { + $status = Migrator::STATUS_PAUSED; + $can_resume = true; + } else { + $status = Migrator::STATUS_RUNNING; + $can_pause = true; + } + } + + $classes = [ + $status, + $can_start ? 'can-start' : '', + $can_pause ? 'can-pause' : '', + $can_resume ? 'can-resume' : '', + ]; + + $data = wp_parse_args([ + 'description' => $migration['description'], + 'status' => $status, + 'classes' => implode( ' ', array_filter($classes) ), + 'status_text' => self::get_status_text($status), + 'message' => $this->_get_migration_message($id, $migration), + 'can_start' => $can_start, + 'can_pause' => $can_pause, + 'can_resume' => $can_resume, + ], $defaults); + + $migrations[$id] = $data; + } + + return $migrations; + } + + /** + * Outputs 'Data Updates' section on the Status tab on the Settings page. + */ + public function tab_content() { + $migrations = $this->_get_migrations_state(); + + if ( !empty($migrations) ) { + $migrations = Helper::array_of_objects($migrations); + + include ud_get_stateless_media()->path('static/views/status-sections/migrations.php', 'dir'); + } + } + + /** + * Get migration state + * + * @param array $state + * @return array + */ + public function migrations_state($state, $params) { + $is_running = $state['is_running'] ?? false; + $is_migration = $state['is_migration'] ?? false; + $force_migrations = $params['force_migrations'] ?? false; + + if ( !$is_running && !$is_migration && !$force_migrations ) { + return $state; + } + + $state['migrations'] = $this->_get_migrations_state($state); + $state['migrations_notify'] = get_site_option(Migrator::MIGRATIONS_NOTIFY_KEY, false); + + return $state; + } +} + \ No newline at end of file diff --git a/lib/classes/sync/class-background-sync.php b/lib/classes/sync/class-background-sync.php index 490fd3a75..8d61bab47 100644 --- a/lib/classes/sync/class-background-sync.php +++ b/lib/classes/sync/class-background-sync.php @@ -119,7 +119,7 @@ public function maybe_handle() { // Don't lock up other requests while processing session_write_close(); - if ($this->is_process_running()) { + if ($this->is_processing()) { // Background process already running. wp_die(); } @@ -356,15 +356,14 @@ protected function complete() { $this->clear_queue_size(); delete_site_option($this->get_stopped_option_key()); - if ($admin_email = get_option('admin_email')) { - $sync_name = strip_tags($this->get_name()); - $site = site_url(); - wp_mail( - $admin_email, - sprintf(__('Stateless Sync for %s is Complete', ud_get_stateless_media()->domain), $sync_name), - sprintf(__("This is a simple notification to inform you that the WP-Stateless plugin has finished a %s synchronization process for %s.\n\nIf you have WP_STATELESS_SYNC_LOG or WP_DEBUG_LOG enabled, check those logs to review any errors that may have occurred during the synchronization process.", ud_get_stateless_media()->domain), $sync_name, $site) - ); - } + // Sending notification + $sync_name = strip_tags($this->get_name()); + $site = site_url(); + + $subject = sprintf(__('Stateless Sync for %s is Complete', ud_get_stateless_media()->domain), $sync_name); + $message = sprintf(__("This is a simple notification to inform you that the WP-Stateless plugin has finished a %s synchronization process for %s.\n\nIf you have WP_STATELESS_SYNC_LOG or WP_DEBUG_LOG enabled, check those logs to review any errors that may have occurred during the synchronization process.", ud_get_stateless_media()->domain), $sync_name, $site); + + do_action('wp_stateless_send_admin_email', $subject, $message); } /** @@ -418,7 +417,7 @@ public function get_process_notice() { * Is running? */ public function is_running() { - return !$this->is_queue_empty() || $this->is_process_running(); + return !$this->is_queue_empty() || $this->is_processing(); } /** diff --git a/lib/classes/sync/class-library-sync.php b/lib/classes/sync/class-library-sync.php index 48b1f833b..9fb3777f1 100644 --- a/lib/classes/sync/class-library-sync.php +++ b/lib/classes/sync/class-library-sync.php @@ -27,7 +27,7 @@ private function get_total_items_trans_key() { */ public final function start($args = []) { try { - if ($this->is_process_running()) throw new UnprocessableException(__('Process already running', ud_get_stateless_media()->domain)); + if ($this->is_processing()) throw new UnprocessableException(__('Process already running', ud_get_stateless_media()->domain)); // Make sure there is no orphaned data and state delete_site_option($this->get_stopped_option_key()); diff --git a/lib/classes/sync/class-non-library-sync.php b/lib/classes/sync/class-non-library-sync.php index 0afed6b10..83ffe2e11 100644 --- a/lib/classes/sync/class-non-library-sync.php +++ b/lib/classes/sync/class-non-library-sync.php @@ -57,7 +57,7 @@ public function get_helper_window() { */ public function start() { try { - if ($this->is_process_running()) throw new UnprocessableException(__('Process already running', ud_get_stateless_media()->domain)); + if ($this->is_processing()) throw new UnprocessableException(__('Process already running', ud_get_stateless_media()->domain)); // Make sure there is no orphaned data and state delete_site_option($this->get_stopped_option_key()); diff --git a/lib/cli/class-sm-cli-command.php b/lib/cli/class-sm-cli-command.php index 4af0fbce9..55e4a5db9 100644 --- a/lib/cli/class-sm-cli-command.php +++ b/lib/cli/class-sm-cli-command.php @@ -239,6 +239,168 @@ public function upgrade($args, $assoc_args) { } } + /** + * Run migrations + * + * ## OPTIONS + * + * [] + * : start migration by its ID + * + * --force + * : Force starting migration even if it is not pending + * + * --progress= + * : Monitor migration progress every seconds (minimum 1) + * + * --email= + * : Send email notification to specified email when migration is finished. By default it uses email from plugin settings. You can also use a list of emails, comma separated. + * + * --url + * : Blog URL if multisite installation. + * + * ## EXAMPLES + * + * wp stateless migrate + * : List migrations information. + * + * wp stateless migrate --url=example.com + * : List migrations information for specific blog in multisite network. + * + * wp stateless migrate --progress=3 + * : Display current migration progress every 3 seconds. + * + * wp stateless migrate 20240216150177 + * : Start migration with ID 20240216150177. + * + * wp stateless migrate 20240216150177 --progress=2 + * : Start migration with ID 20240216150177 and display progress every 2 seconds. + * + * wp stateless migrate 20240216150177 --force --email=mail@example.com,user@domain.com --url=example.com + * : Start migration with ID 20240216150177 for specific blog in multisite network. Start migration even if it was already finished or failed. After finishing send email notification to mail@example.com and user@domain.com. + * + * @synopsis [] [--force] [--progress=] [--email=] [--url=] + * @param $args + * @param $assoc_args + */ + public function migrate($args, $assoc_args) { + $id = $args[0] ?? ''; + + // No migration ID provided, list all migrations and exit + if ( empty($id) && !isset($assoc_args['progress']) ) { + $this->_list_migrations(); + + return; + } else if ( !empty($id) ) { + // Check if we can run migration + $migrations = $this->_get_migrations(); + + if ( !isset($migrations[$id]) ) { + WP_CLI::error("Invalid migration ID: $id"); + } + + if ( !$migrations[$id]['can_start'] && !isset($assoc_args['force']) ) { + WP_CLI::error( 'Migration ' . $migrations[$id]['description'] . ' is not ready for starting. ' . PHP_EOL . + 'Migration status: ' . $migrations[$id]['status_text'] . ', ' . strip_tags($migrations[$id]['message']) . PHP_EOL . + 'Please use --force to run it anyway.' + ); + } + + $email = $assoc_args['email'] ?? ud_get_stateless_media()->get_notification_email(); + + WP_CLI::confirm( 'Please make a backup copy of your database and try not to upload, change or delete your media while the process continues.' . PHP_EOL . + "After the process finishes an email will be sent to: $email" . PHP_EOL . + "Are you sure you want to run the migration $id?", + $assoc_args + ); + + // Run migration + \wpCloud\StatelessMedia\Migrator::instance()->start_migration([], [ + 'id' => $id, + 'is_migration' => true, + 'force' => true, + ]); + } + + if ( isset($assoc_args['progress']) ) { + $this->_check_progress($assoc_args['progress']); + } + } + + /** + * Get migrations state + * + * @return array + */ + private function _get_migrations() { + $migrations = apply_filters('wp_stateless_batch_state', [], ['force_migrations' => true]); + return $migrations['migrations'] ?? []; + } + + /** + * List migrations + */ + private function _list_migrations() { + $migrations = $this->_get_migrations(); + + if ( empty($migrations) ) { + WP_CLI::success('No migrations found'); + } + + $data = []; + + foreach ($migrations as $id => $migration) { + $data[$id] = [ + 'id' => $id, + 'description' => $migration['description'], + 'status' => $migration['status_text'], + 'message' => strip_tags($migration['message']), + ]; + } + + WP_CLI\Utils\format_items('table', $data, ['id', 'description', 'status', 'message']); + } + + /** + * Check progress + */ + private function _check_progress($progress) { + global $wpdb; + + $sleep = max($progress, 1); + $key = \wpCloud\StatelessMedia\Batch\BatchTaskManager::instance()->get_state_key(); + + if ( is_multisite() ) { + $sql = "SELECT meta_value FROM $wpdb->sitemeta WHERE meta_key = '%s' AND site_id = %d LIMIT 1"; + $sql = $wpdb->prepare($sql, $key, get_current_network_id()); + } else { + $sql = "SELECT option_value FROM $wpdb->options WHERE option_name = '%s' LIMIT 1"; + $sql = $wpdb->prepare($sql, $key); + } + + do { + // We need to omit the cache and get the data directly from the db + $state = $wpdb->get_var($sql); + $state = maybe_unserialize($state); + + if ( empty($state) || !isset($state['is_migration']) || !$state['is_migration'] ) { + WP_CLI::success('No migration in progress'); + return; + } + + $description = $state['description'] ?? ''; + $completed = $state['completed'] ?? 0; + $total = $state['total'] ?? 0; + + $percent = $total > 0 ? round($completed / $total * 100, 2) : 0; + + $message = sprintf("Migration '%s' compeleted %.2f%%: %d of %d items processed", $description, $percent, $completed, $total); + WP_CLI::line($message); + + sleep($sleep); + } while (true); + } + /** * Runs batches */ diff --git a/lib/ns-vendor/classes/deliciousbrains/wp-background-processing/classes/wp-async-request.php b/lib/ns-vendor/classes/deliciousbrains/wp-background-processing/classes/wp-async-request.php index 85645346c..135aa6550 100644 --- a/lib/ns-vendor/classes/deliciousbrains/wp-background-processing/classes/wp-async-request.php +++ b/lib/ns-vendor/classes/deliciousbrains/wp-background-processing/classes/wp-async-request.php @@ -1,5 +1,4 @@ identifier = $this->prefix . '_' . $this->action; - add_action('wp_ajax_' . $this->identifier, array($this, 'maybe_handle')); - add_action('wp_ajax_nopriv_' . $this->identifier, array($this, 'maybe_handle')); + add_action( 'wp_ajax_' . $this->identifier, array( $this, 'maybe_handle' ) ); + add_action( 'wp_ajax_nopriv_' . $this->identifier, array( $this, 'maybe_handle' ) ); } /** - * Set data used during the request + * Set data used during the request. * * @param array $data Data. * * @return $this */ - public function data($data) { + public function data( $data ) { $this->data = $data; return $this; } /** - * Dispatch the async request + * Dispatch the async request. * - * @return array|WP_Error + * @return array|WP_Error|false HTTP Response array, WP_Error on failure, or false if not attempted. */ public function dispatch() { - $url = add_query_arg($this->get_query_args(), $this->get_query_url()); + $url = add_query_arg( $this->get_query_args(), $this->get_query_url() ); $args = $this->get_post_args(); - return wp_remote_post(esc_url_raw($url), $args); + return wp_remote_post( esc_url_raw( $url ), $args ); } /** - * Get query args + * Get query args. * * @return array */ protected function get_query_args() { - if (property_exists($this, 'query_args')) { + if ( property_exists( $this, 'query_args' ) ) { return $this->query_args; } $args = array( 'action' => $this->identifier, - 'nonce' => wp_create_nonce($this->identifier), + 'nonce' => wp_create_nonce( $this->identifier ), ); /** @@ -106,36 +105,36 @@ protected function get_query_args() { * * @param array $url */ - return apply_filters($this->identifier . '_query_args', $args); + return apply_filters( $this->identifier . '_query_args', $args ); } /** - * Get query URL + * Get query URL. * * @return string */ protected function get_query_url() { - if (property_exists($this, 'query_url')) { + if ( property_exists( $this, 'query_url' ) ) { return $this->query_url; } - $url = admin_url('admin-ajax.php'); + $url = admin_url( 'admin-ajax.php' ); /** * Filters the post arguments used during an async request. * * @param string $url */ - return apply_filters($this->identifier . '_query_url', $url); + return apply_filters( $this->identifier . '_query_url', $url ); } /** - * Get post args + * Get post args. * * @return array */ protected function get_post_args() { - if (property_exists($this, 'post_args')) { + if ( property_exists( $this, 'post_args' ) ) { return $this->post_args; } @@ -143,8 +142,8 @@ protected function get_post_args() { 'timeout' => 0.01, 'blocking' => false, 'body' => $this->data, - 'cookies' => $_COOKIE, - 'sslverify' => apply_filters('https_local_ssl_verify', false), + 'cookies' => $_COOKIE, // Passing cookies ensures request is performed as initiating user. + 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), // Local requests, fine to pass false. ); /** @@ -152,27 +151,49 @@ protected function get_post_args() { * * @param array $args */ - return apply_filters($this->identifier . '_post_args', $args); + return apply_filters( $this->identifier . '_post_args', $args ); } /** - * Maybe handle + * Maybe handle a dispatched request. * * Check for correct nonce and pass to handler. + * + * @return void|mixed */ public function maybe_handle() { - // Don't lock up other requests while processing + // Don't lock up other requests while processing. session_write_close(); - check_ajax_referer($this->identifier, 'nonce'); + check_ajax_referer( $this->identifier, 'nonce' ); $this->handle(); - wp_die(); + return $this->maybe_wp_die(); + } + + /** + * Should the process exit with wp_die? + * + * @param mixed $return What to return if filter says don't die, default is null. + * + * @return void|mixed + */ + protected function maybe_wp_die( $return = null ) { + /** + * Should wp_die be used? + * + * @return bool + */ + if ( apply_filters( $this->identifier . '_wp_die', true ) ) { + wp_die(); + } + + return $return; } /** - * Handle + * Handle a dispatched request. * * Override this method to perform any actions required * during the async request. diff --git a/lib/ns-vendor/classes/deliciousbrains/wp-background-processing/classes/wp-background-process.php b/lib/ns-vendor/classes/deliciousbrains/wp-background-processing/classes/wp-background-process.php index 7a9844da8..c0b968175 100644 --- a/lib/ns-vendor/classes/deliciousbrains/wp-background-processing/classes/wp-background-process.php +++ b/lib/ns-vendor/classes/deliciousbrains/wp-background-processing/classes/wp-background-process.php @@ -36,7 +36,7 @@ abstract class UDX_WP_Background_Process extends UDX_WP_Async_Request { /** * Cron_hook_identifier * - * @var mixed + * @var string * @access protected */ protected $cron_hook_identifier; @@ -44,13 +44,27 @@ abstract class UDX_WP_Background_Process extends UDX_WP_Async_Request { /** * Cron_interval_identifier * - * @var mixed + * @var string * @access protected */ protected $cron_interval_identifier; /** - * Initiate new background process + * The status set when process is cancelling. + * + * @var int + */ + const STATUS_CANCELLED = 1; + + /** + * The status set when process is paused or pausing. + * + * @var int; + */ + const STATUS_PAUSED = 2; + + /** + * Initiate new background process. */ public function __construct() { parent::__construct(); @@ -63,12 +77,17 @@ public function __construct() { } /** - * Dispatch + * Schedule the cron healthcheck and dispatch an async request to start processing the queue. * * @access public - * @return void + * @return array|WP_Error|false HTTP Response array, WP_Error on failure, or false if not attempted. */ public function dispatch() { + if ( $this->is_processing() ) { + // Process already running. + return false; + } + // Schedule the cron healthcheck. $this->schedule_event(); @@ -77,7 +96,9 @@ public function dispatch() { } /** - * Push to queue + * Push to the queue. + * + * Note, save must be called in order to persist queued items to a batch for processing. * * @param mixed $data Data. * @@ -90,7 +111,7 @@ public function push_to_queue( $data ) { } /** - * Save queue + * Save the queued items for future processing. * * @return $this */ @@ -101,11 +122,14 @@ public function save() { update_site_option( $key, $this->data ); } + // Clean out data so that new data isn't prepended with closed session's data. + $this->data = array(); + return $this; } /** - * Update queue + * Update a batch's queued items. * * @param string $key Key. * @param array $data Data. @@ -121,7 +145,7 @@ public function update( $key, $data ) { } /** - * Delete queue + * Delete a batch of queued items. * * @param string $key Key. * @@ -134,83 +158,215 @@ public function delete( $key ) { } /** - * Generate key + * Delete entire job queue. + */ + public function delete_all() { + $batches = $this->get_batches(); + + foreach ( $batches as $batch ) { + $this->delete( $batch->key ); + } + + delete_site_option( $this->get_status_key() ); + + $this->cancelled(); + } + + /** + * Cancel job on next batch. + */ + public function cancel() { + update_site_option( $this->get_status_key(), self::STATUS_CANCELLED ); + + // Just in case the job was paused at the time. + $this->dispatch(); + } + + /** + * Has the process been cancelled? + * + * @return bool + */ + public function is_cancelled() { + $status = get_site_option( $this->get_status_key(), 0 ); + + if ( absint( $status ) === self::STATUS_CANCELLED ) { + return true; + } + + return false; + } + + /** + * Called when background process has been cancelled. + */ + protected function cancelled() { + do_action( $this->identifier . '_cancelled' ); + } + + /** + * Pause job on next batch. + */ + public function pause() { + update_site_option( $this->get_status_key(), self::STATUS_PAUSED ); + } + + /** + * Is the job paused? + * + * @return bool + */ + public function is_paused() { + $status = get_site_option( $this->get_status_key(), 0 ); + + if ( absint( $status ) === self::STATUS_PAUSED ) { + return true; + } + + return false; + } + + /** + * Called when background process has been paused. + */ + protected function paused() { + do_action( $this->identifier . '_paused' ); + } + + /** + * Resume job. + */ + public function resume() { + delete_site_option( $this->get_status_key() ); + + $this->schedule_event(); + $this->dispatch(); + $this->resumed(); + } + + /** + * Called when background process has been resumed. + */ + protected function resumed() { + do_action( $this->identifier . '_resumed' ); + } + + /** + * Is queued? + * + * @return bool + */ + public function is_queued() { + return ! $this->is_queue_empty(); + } + + /** + * Is the tool currently active, e.g. starting, working, paused or cleaning up? + * + * @return bool + */ + public function is_active() { + return $this->is_queued() || $this->is_processing() || $this->is_paused() || $this->is_cancelled(); + } + + /** + * Generate key for a batch. * * Generates a unique key based on microtime. Queue items are * given a unique key so that they can be merged upon save. * - * @param int $length Length. + * @param int $length Optional max length to trim key to, defaults to 64 characters. + * @param string $key Optional string to append to identifier before hash, defaults to "batch". * * @return string */ - protected function generate_key( $length = 64 ) { - $unique = md5( microtime() . rand() ); - $prepend = $this->identifier . '_batch_'; + protected function generate_key( $length = 64, $key = 'batch' ) { + $unique = md5( microtime() . wp_rand() ); + $prepend = $this->identifier . '_' . $key . '_'; return substr( $prepend . $unique, 0, $length ); } /** - * Maybe process queue + * Get the status key. + * + * @return string + */ + protected function get_status_key() { + return $this->identifier . '_status'; + } + + /** + * Maybe process a batch of queued items. * * Checks whether data exists within the queue and that * the process is not already running. */ public function maybe_handle() { - // Don't lock up other requests while processing + // Don't lock up other requests while processing. session_write_close(); - if ( $this->is_process_running() ) { + if ( $this->is_processing() ) { // Background process already running. - wp_die(); + return $this->maybe_wp_die(); + } + + if ( $this->is_cancelled() ) { + $this->clear_scheduled_event(); + $this->delete_all(); + + return $this->maybe_wp_die(); + } + + if ( $this->is_paused() ) { + $this->clear_scheduled_event(); + $this->paused(); + + return $this->maybe_wp_die(); } if ( $this->is_queue_empty() ) { // No data to process. - wp_die(); + return $this->maybe_wp_die(); } check_ajax_referer( $this->identifier, 'nonce' ); $this->handle(); - wp_die(); + return $this->maybe_wp_die(); } /** - * Is queue empty + * Is queue empty? * * @return bool */ protected function is_queue_empty() { - global $wpdb; - - $table = $wpdb->options; - $column = 'option_name'; - - if ( is_multisite() ) { - $table = $wpdb->sitemeta; - $column = 'meta_key'; - } - - $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%'; - - $count = $wpdb->get_var( $wpdb->prepare( " - SELECT COUNT(*) - FROM {$table} - WHERE {$column} LIKE %s - ", $key ) ); - - return ( $count > 0 ) ? false : true; + return empty( $this->get_batch() ); } /** - * Is process running + * Is process running? * * Check whether the current process is already running * in a background process. + * + * @return bool + * + * @deprecated 1.1.0 Superseded. + * @see is_processing() */ protected function is_process_running() { + return $this->is_processing(); + } + + /** + * Is the background process currently running? + * + * @return bool + */ + public function is_processing() { if ( get_site_transient( $this->identifier . '_process_lock' ) ) { // Process already running. return true; @@ -220,7 +376,7 @@ protected function is_process_running() { } /** - * Lock process + * Lock process. * * Lock the process so that multiple instances can't run simultaneously. * Override if applicable, but the duration should be greater than that @@ -236,7 +392,7 @@ protected function lock_process() { } /** - * Unlock process + * Unlock process. * * Unlock the process so that other instances can spawn. * @@ -249,13 +405,34 @@ protected function unlock_process() { } /** - * Get batch + * Get batch. * - * @return stdClass Return the first batch from the queue + * @return stdClass Return the first batch of queued items. */ protected function get_batch() { + return array_reduce( + $this->get_batches( 1 ), + function ( $carry, $batch ) { + return $batch; + }, + array() + ); + } + + /** + * Get batches. + * + * @param int $limit Number of batches to return, defaults to all. + * + * @return array of stdClass + */ + public function get_batches( $limit = 0 ) { global $wpdb; + if ( empty( $limit ) || ! is_int( $limit ) ) { + $limit = 0; + } + $table = $wpdb->options; $column = 'option_name'; $key_column = 'option_id'; @@ -270,23 +447,43 @@ protected function get_batch() { $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%'; - $query = $wpdb->get_row( $wpdb->prepare( " + $sql = ' SELECT * - FROM {$table} - WHERE {$column} LIKE %s - ORDER BY {$key_column} ASC - LIMIT 1 - ", $key ) ); + FROM ' . $table . ' + WHERE ' . $column . ' LIKE %s + ORDER BY ' . $key_column . ' ASC + '; + + $args = array( $key ); + + if ( ! empty( $limit ) ) { + $sql .= ' LIMIT %d'; + + $args[] = $limit; + } + + $items = $wpdb->get_results( $wpdb->prepare( $sql, $args ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + + $batches = array(); - $batch = new stdClass(); - $batch->key = $query->$column; - $batch->data = maybe_unserialize( $query->$value_column ); + if ( ! empty( $items ) ) { + $batches = array_map( + function ( $item ) use ( $column, $value_column ) { + $batch = new stdClass(); + $batch->key = $item->{$column}; + $batch->data = maybe_unserialize( $item->{$value_column} ); + + return $batch; + }, + $items + ); + } - return $batch; + return $batches; } /** - * Handle + * Handle a dispatched request. * * Pass each queue item to the task handler, while remaining * within server memory and time limit constraints. @@ -294,6 +491,22 @@ protected function get_batch() { protected function handle() { $this->lock_process(); + /** + * Number of seconds to sleep between batches. Defaults to 0 seconds, minimum 0. + * + * @param int $seconds + */ + $throttle_seconds = max( + 0, + apply_filters( + $this->identifier . '_seconds_between_batches', + apply_filters( + $this->prefix . '_seconds_between_batches', + 0 + ) + ) + ); + do { $batch = $this->get_batch(); @@ -306,19 +519,25 @@ protected function handle() { unset( $batch->data[ $key ] ); } - if ( $this->time_exceeded() || $this->memory_exceeded() ) { - // Batch limits reached. + // Keep the batch up to date while processing it. + if ( ! empty( $batch->data ) ) { + $this->update( $batch->key, $batch->data ); + } + + // Let the server breathe a little. + sleep( $throttle_seconds ); + + // Batch limits reached, or pause or cancel request. + if ( $this->time_exceeded() || $this->memory_exceeded() || $this->is_paused() || $this->is_cancelled() ) { break; } } - // Update or delete current batch. - if ( ! empty( $batch->data ) ) { - $this->update( $batch->key, $batch->data ); - } else { + // Delete current batch if fully processed. + if ( empty( $batch->data ) ) { $this->delete( $batch->key ); } - } while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() ); + } while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() && ! $this->is_paused() && ! $this->is_cancelled() ); $this->unlock_process(); @@ -329,11 +548,11 @@ protected function handle() { $this->complete(); } - wp_die(); + return $this->maybe_wp_die(); } /** - * Memory exceeded + * Memory exceeded? * * Ensures the batch process never exceeds 90% * of the maximum WordPress memory. @@ -353,7 +572,7 @@ protected function memory_exceeded() { } /** - * Get memory limit + * Get memory limit in bytes. * * @return int */ @@ -365,7 +584,7 @@ protected function get_memory_limit() { $memory_limit = '128M'; } - if ( ! $memory_limit || - 1 === intval( $memory_limit ) ) { + if ( ! $memory_limit || -1 === intval( $memory_limit ) ) { // Unlimited, set to 32GB. $memory_limit = '32000M'; } @@ -374,7 +593,7 @@ protected function get_memory_limit() { } /** - * Time exceeded. + * Time limit exceeded? * * Ensures the batch never exceeds a sensible time limit. * A timeout limit of 30s is common on shared hosting. @@ -393,18 +612,29 @@ protected function time_exceeded() { } /** - * Complete. + * Complete processing. * * Override if applicable, but ensure that the below actions are * performed, or, call parent::complete(). */ protected function complete() { - // Unschedule the cron healthcheck. + delete_site_option( $this->get_status_key() ); + + // Remove the cron healthcheck job from the cron schedule. $this->clear_scheduled_event(); + + $this->completed(); + } + + /** + * Called when background process has completed. + */ + protected function completed() { + do_action( $this->identifier . '_completed' ); } /** - * Schedule cron healthcheck + * Schedule the cron healthcheck job. * * @access public * @@ -413,29 +643,35 @@ protected function complete() { * @return mixed */ public function schedule_cron_healthcheck( $schedules ) { - $interval = apply_filters( $this->identifier . '_cron_interval', 5 ); + $interval = apply_filters( $this->cron_interval_identifier, 5 ); if ( property_exists( $this, 'cron_interval' ) ) { - $interval = apply_filters( $this->identifier . '_cron_interval', $this->cron_interval ); + $interval = apply_filters( $this->cron_interval_identifier, $this->cron_interval ); } - // Adds every 5 minutes to the existing schedules. - $schedules[ $this->identifier . '_cron_interval' ] = array( + if ( 1 === $interval ) { + $display = __( 'Every Minute' ); + } else { + $display = sprintf( __( 'Every %d Minutes' ), $interval ); + } + + // Adds an "Every NNN Minute(s)" schedule to the existing cron schedules. + $schedules[ $this->cron_interval_identifier ] = array( 'interval' => MINUTE_IN_SECONDS * $interval, - 'display' => sprintf( __( 'Every %d Minutes' ), $interval ), + 'display' => $display, ); return $schedules; } /** - * Handle cron healthcheck + * Handle cron healthcheck event. * * Restart the background process if not already running * and data exists in the queue. */ public function handle_cron_healthcheck() { - if ( $this->is_process_running() ) { + if ( $this->is_processing() ) { // Background process already running. exit; } @@ -446,13 +682,11 @@ public function handle_cron_healthcheck() { exit; } - $this->handle(); - - exit; + $this->dispatch(); } /** - * Schedule event + * Schedule the cron healthcheck event. */ protected function schedule_event() { if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) { @@ -461,7 +695,7 @@ protected function schedule_event() { } /** - * Clear scheduled event + * Clear scheduled cron healthcheck event. */ protected function clear_scheduled_event() { $timestamp = wp_next_scheduled( $this->cron_hook_identifier ); @@ -472,24 +706,19 @@ protected function clear_scheduled_event() { } /** - * Cancel Process + * Cancel the background process. * - * Stop processing queue items, clear cronjob and delete batch. + * Stop processing queue items, clear cron job and delete batch. * + * @deprecated 1.1.0 Superseded. + * @see cancel() */ public function cancel_process() { - if ( ! $this->is_queue_empty() ) { - $batch = $this->get_batch(); - - $this->delete( $batch->key ); - - wp_clear_scheduled_hook( $this->cron_hook_identifier ); - } - + $this->cancel(); } /** - * Task + * Perform task with queued item. * * Override this method to perform any actions required on each * queue item. Return the modified item for further processing @@ -501,5 +730,4 @@ public function cancel_process() { * @return mixed */ abstract protected function task( $item ); - -} \ No newline at end of file +} diff --git a/readme.md b/readme.md index 343a1dca0..1c8505813 100644 --- a/readme.md +++ b/readme.md @@ -33,6 +33,10 @@ New to Google Cloud? Google is offering you a [$300 credit](https://console.clou * Serverless platform compatible, including Google App Engine. * Multisite compatible. +### Addons +* [BuddyBoss Platform Addon](https://wordpress.org/plugins/wp-stateless-buddyboss-platform-addon/) +* [Elementor Website Builder Addon](https://wordpress.org/plugins/wp-stateless-elementor-website-builder-addon/) + ### Support, Feedback, & Contribute We welcome community involvement via the [GitHub repository](https://github.com/udx/wp-stateless). diff --git a/readme.txt b/readme.txt index ff8238722..1ff3c24f5 100644 --- a/readme.txt +++ b/readme.txt @@ -1,12 +1,12 @@ === WP-Stateless - Google Cloud Storage === Contributors: usability_dynamics, andypotanin, ideric, maxim.peshkov, planvova, obolgun Donate link: https://udx.io -Tags: google, google cloud, google cloud storage, cdn, uploads, media, stateless, backup +Tags: google cloud, google cloud storage, cdn, uploads, backup License: GPLv2 or later Requires PHP: 8.0 Requires at least: 5.0 Tested up to: 6.4.3 -Stable tag: 3.4.1 +Stable tag: 4.0.0-RC.1 Upload and serve your WordPress media files from Google Cloud Storage. @@ -43,6 +43,10 @@ New to Google Cloud? Google is offering you a [$300 credit](https://console.clou * Serverless platform compatible, including Google App Engine. * Multisite compatible. += Addons = +* [BuddyBoss Platform Addon](https://wordpress.org/plugins/wp-stateless-buddyboss-platform-addon/) +* [Elementor Website Builder Addon](https://wordpress.org/plugins/wp-stateless-elementor-website-builder-addon/) + = Support, Feedback, & Contribute = We welcome community involvement via the [GitHub repository](https://github.com/udx/wp-stateless). @@ -102,6 +106,11 @@ To ensure new releases cause as little disruption as possible, we rely on a numb == Upgrade Notice == += 4.0.0 = +You will be prompted to run data optimization after upgrade. Please make a backup copy of your database. +If you using BuddyBoss Platform you will be proposed to install [WP-Stateless – BuddyBoss Platform Addon](https://wordpress.org/plugins/wp-stateless-buddyboss-platform-addon/), which replaces BuddyBoss Compatibility. +If you using Elementor Website Builder you will be proposed to install [WP-Stateless – Elementor Website Builder Addon](https://wordpress.org/plugins/wp-stateless-elementor-website-builder-addon/), which replaces Elementor Compatibility. + = 3.2.3 = Before upgrading to WP-Stateless 3.2.3, please, make sure you use PHP 8.0 or above. @@ -112,8 +121,28 @@ Before upgrading to WP-Stateless 3.2.0, please, make sure you use PHP 7.2 or abo Before upgrading to WP-Stateless 3.0, please, make sure you tested it on your development environment. == Changelog == += 4.0.0 = +* NEW - use custom database tables to store GCS file data. This increases plugin performance and will be used for future improvements. +* NEW - added filter `wp_stateless_get_file`, retrieves the GCS file data, should be used instead of getting `sm_cloud` postmeta directly. +* NEW - added filter `wp_stateless_get_file_sizes`, retrieves the GCS file data for image sizes, should be used instead of getting `sm_cloud` postmeta directly. +* NEW - added filter `wp_stateless_get_file_meta`, retrieves all GCS file meta data, should be used instead of getting `sm_cloud` postmeta directly. +* NEW - added filter `wp_stateless_get_file_meta_value`, retrieves the GCS file meta data by meta_key, should be used instead of getting `sm_cloud` postmeta directly. +* NEW - added setting allowing to change email for WP-Stateless notifications. +* NEW - added new Settings tab `Addons`, which contains the list of WP-Stateless Addons, which replace Compatibilities. +* NEW - added new Settings tab `Status`, which contains status and health information related to Google Cloud Storage and WP-Stateless. +* NEW - CLI command `wp stateless migrate` to list and operate data optimizations. +* NEW - configuration constant [`WP_STATELESS_POSTMETA`](https://stateless.udx.io/docs/constants/#wp_stateless_postmeta) allows to read the GCS file data from postmeta instead of the new custom database tables. +* NEW - configuration constant [`WP_STATELESS_BATCH_HEALTHCHECK_INTERVAL`](https://stateless.udx.io/docs/constants/#wp_stateless_batch_healthcheck_interval) defines an interval in minutes for periodical health checks of a batch background process (like data optimization). +* COMPATIBILITY - BuddyBoss Compatibility replaced with [WP-Stateless – BuddyBoss Platform Addon](https://wordpress.org/plugins/wp-stateless-buddyboss-platform-addon/). +* COMPATIBILITY - Elementor Compatibility replaced with [WP-Stateless – Elementor Website Builder Addon](https://wordpress.org/plugins/wp-stateless-elementor-website-builder-addon/). +* COMPATIBILITY - Gravity Form Compatibility does not support older version of Gravity Forms (< 2.3). +* ENHANCEMENT - Allow dismissing notices in Admin Panel only for logged in users. +* ENHANCEMENT - Updated `wp-background-processing` library from from 1.0.2 to 1.1.1. +* ENHANCEMENT - Updated `phpseclib` 3.0.34 to 3.0.37. +* FIX - proper use of infinite timeout in `set_time_limit` function to avoid issues with PHP 8.1 and above [#704](https://github.com/udx/wp-stateless/issues/704). + = 3.4.1 = -FIX - improve security while processing AJAX requests in Admin Panel +* FIX - improve security while processing AJAX requests in Admin Panel = 3.4.0 = * ENHANCEMENT - removed `udx/lib-settings` package dependency for security reasons. diff --git a/static/data/addons.php b/static/data/addons.php index abcdc43c0..a27786512 100644 --- a/static/data/addons.php +++ b/static/data/addons.php @@ -20,17 +20,27 @@ */ return [ - /* + 'buddyboss' => [ 'title' => 'BuddyBoss Platform', 'plugin_files' => ['buddyboss-platform/bp-loader.php'], 'addon_file' => 'wp-stateless-buddyboss-addon/wp-stateless-buddyboss-addon.php', 'icon' => 'https://www.buddyboss.com/wp-content/uploads/2022/04/bb-logo-1.png', 'repo' => 'udx/wp-stateless-buddyboss-addon', - 'wp' => 'https://wordpress.org/plugins/wp-stateless/', + 'wp' => 'https://wordpress.org/plugins/wp-stateless-buddyboss-platform-addon/', 'hubspot_id' => '151481399845', 'hubspot_link' => 'https://cta-service-cms2.hubspot.com/web-interactives/public/v1/track/click?encryptedPayload=AVxigLIz%2BcFUMcIBKQ7Xqj0pOF0COKC9I0GezkxwgHqPgiPgyfhisc6veCbNsRloVLAajjD9D%2ByVhIPRFdsFfxJbmC96vdcpZbFUIqn%2F2qS7eXcpXHENalnSIMHrRy3vZ25OujO7MQ8WgbQMNJlTJJ9N0%2FyC6UbEjKMWdWjvjXnAPRh5giepyw2JtqMqgupq85f5rhzgYJgXJKOAzaOwja%2Bedw%3D%3D&portalId=20504491', ], - */ + + 'elementor' => [ + 'title' => 'Elementor Website Builder', + 'plugin_files' => ['elementor/elementor.php'], + 'addon_file' => 'wp-stateless-elementor-addon/wp-stateless-elementor-addon.php', + 'icon' => 'https://ps.w.org/elementor/assets/icon.svg', + 'repo' => 'udx/wp-stateless-elementor-addon', + 'wp' => 'https://wordpress.org/plugins/wp-stateless-elementor-website-builder-addon/', + 'hubspot_id' => '151481399819', + 'hubspot_link' => 'https://cta-service-cms2.hubspot.com/web-interactives/public/v1/track/click?encryptedPayload=AVxigLKR8B2Z9422V%2Fh9SGpptZeq1UWUETejTC8i1C7YoBj8TRWSG2Yij36fQHaj37NIgIU0OgWeZ9SAaTb9lL%2BlPaEKwWJ1WcQNWv%2FLFWh1Y8LTEIUGRvPzShNKyv0yIC5Z3Hu6YWGYp46iXXI6nLLBfbt2fHytn3mHX7Ic3%2ByuAF3Cz2rmMusOMD3XSJGTAYobOOXuyHJzeHzztZAimflHRg%3D%3D&portalId=20504491', + ], ]; diff --git a/static/migrations/20240219175240.php b/static/migrations/20240219175240.php new file mode 100644 index 000000000..fb5270a2d --- /dev/null +++ b/static/migrations/20240219175240.php @@ -0,0 +1,358 @@ +posts posts " . + "WHERE posts.post_type = 'attachment' " . + "ORDER BY ID DESC " . + "LIMIT 1"; + + $result = $wpdb->get_var( $sql ); + + return !empty( $result ); + } + + public function init_state() { + global $wpdb; + + parent::init_state(); + + $this->description = __( "Update data for Google Cloud files", ud_get_stateless_media()->domain ); + + $order = self::DESC ? 'DESC' : 'ASC'; + + // Getting the first/last attachment ID as a starting point + $sql = "SELECT ID " . + "FROM $wpdb->posts posts " . + "WHERE posts.post_type = 'attachment' " . + "ORDER BY ID $order " . + "LIMIT 1"; + + $start_id = $wpdb->get_var( $sql ); + + // Getting the total number of attachments + $sql = "SELECT COUNT(*) " . + "FROM $wpdb->posts posts " . + "WHERE posts.post_type = 'attachment' AND posts.post_status != 'trash'"; + + $total = $wpdb->get_var( $sql ); + + $this->total = self::LIMIT > 0 ? min( self::LIMIT, $total ) : $total; + $this->limit = self::BATCH_SIZE; + $this->offset = self::DESC ? $start_id + 1 : $start_id - 1; + } + + public function get_batch() { + global $wpdb; + + $batch = []; + + if ( self::LIMIT > 0 && $this->completed >= self::LIMIT ) { + $this->stop = true; + return $batch; + } + + if ($this->stop) { + return $batch; + } + + $order = self::DESC ? 'DESC' : 'ASC'; + $condition = self::DESC ? 'posts.ID < %d' : 'posts.ID > %d'; + + // Using last post ID instead of limit for performance + $sql = "SELECT posts.ID " . + "FROM $wpdb->posts posts " . + "WHERE posts.post_type = 'attachment' AND $condition " . + "ORDER BY posts.ID $order " . + "LIMIT %d"; + + $sql = $wpdb->prepare( $sql, $this->offset, $this->limit ); + + $batch = $wpdb->get_col( $sql ); + + $count = count( $batch ); + + $this->offset = end( $batch ); + + if ( $count < $this->limit ) { + $this->stop = true; + } + + return $batch; + } + + public function get_state() { + $state = parent::get_state(); + + $state['stop'] = $this->stop; + + return $state; + } + + public function set_state($state) { + parent::set_state($state); + + $this->stop = $state['stop'] ?? false; + } + + /** + * Get the 'generation' field from the GCS media link + * + * @param string $sm_id + * @return string + */ + private function _get_generation_from_media_link($media_link) { + $query = parse_url($media_link, PHP_URL_QUERY); + parse_str($query, $parts); + + return $parts['generation'] ?? 0; + } + + /** + * Get the file size from the meta or from the older version of meta + * + * @param array $meta + * @return int|null + */ + private function _get_file_size($meta) { + if ( isset( $meta['filesize'] ) ) { + return $meta['filesize']; + } + + if ( isset( $meta['object'] ) && isset( $meta['object']['size'] ) ) { + return $meta['object']['size']; + } + + return null; + } + + /** + * Get the width from the meta or from the WP attachment meta + * + * @param array $meta + * @param array $wp_meta + * @return int|null + */ + private function _get_width($meta, $wp_meta) { + if ( isset( $meta['width'] ) ) { + return $meta['width']; + } + + if ( isset( $meta['object'] ) && isset( $meta['object']['metadata'] ) && isset( $meta['object']['metadata']['width'] ) ) { + return $meta['object']['metadata']['width']; + } + + if ( isset( $wp_meta['width'] ) ) { + return $wp_meta['width']; + } + + return null; + } + + /** + * Get the height from the meta or from the WP attachment meta + * + * @param array $meta + * @param array $wp_meta + * @return int|null + */ + private function _get_height($meta, $wp_meta) { + if ( isset( $meta['height'] ) ) { + return $meta['height']; + } + + if ( isset( $meta['object'] ) && isset( $meta['object']['metadata'] ) && isset( $meta['object']['metadata']['height'] ) ) { + return $meta['object']['metadata']['height']; + } + + if ( isset( $wp_meta['height'] ) ) { + return $wp_meta['height']; + } + + return null; + } + + /** + * Get the content type from the meta or from the file + * + * @param array $meta + * @param string $file + * @return string + * @throws \Exception + */ + private function _get_content_type($meta, $file) { + if ( isset( $meta['contentType'] ) ) { + return $meta['contentType']; + } + + if ( isset( $meta['object'] ) && isset( $meta['object']['contentType'] ) ) { + return $meta['object']['contentType']; + } + + // Get mimetype based on file extension the file extension + $file = pathinfo($file, PATHINFO_BASENAME); + $type = wp_check_filetype($file); + + return $type['type'] ?? ''; + } + + /** + * Get the self link from the meta + * + * @param array $meta + * @return string + * @throws \Exception + */ + private function _get_self_link($meta) { + if ( isset($meta['selfLink']) ) { + return $meta['selfLink']; + } + + if ( !isset($meta['mediaLink']) ) { + throw new \Exception('Media link not defined'); + } + + $link = $meta['mediaLink'] ?? ''; + $remove = '/download'; + + $pos = strpos($link, $remove); + + if ($pos === false) { + return $link; + } + + $link = substr_replace($link, '', $pos, strlen($remove)); + $parts = explode('?', $link); + + return reset($parts); + } + + /** + * Get the version from the meta + * + * @param array $meta + * @return string + */ + private function _get_version($meta) { + return isset($meta['sm_version']) ? $meta['sm_version'] : $this->id; + } + + /** + * Get the file link from the meta or generate a new one + * + * @param string $name + * @param array $meta + * @return string + */ + private function _get_file_link($name, $meta) { + return isset($meta['fileLink']) ? $meta['fileLink'] : ud_stateless_db()->get_file_link($name); + } + + public function process_item($item) { + global $wpdb; + + Helper::log('Processing item ' . $item); + + $meta = get_post_meta( $item, 'sm_cloud', true ); + + if ( !$meta || empty($meta) ) { + return false; + } + + $wp_meta = get_post_meta( $item, '_wp_attachment_metadata', true ); + + // Disable autocommit and use transactions to ensure data integrity + $wpdb->query( 'SET autocommit = 0;' ); + $wpdb->query( 'START TRANSACTION;' ); + + try { + // Update file data + $name = $meta['name'] ?? ''; + + $data = [ + 'post_id' => $item, + 'name' => $name, + 'bucket' => $meta['bucket'] ?? '', + 'generation' => $this->_get_generation_from_media_link( $meta['mediaLink'] ?? ''), + 'cache_control' => $meta['cacheControl'] ?? null, + 'content_type' => $this->_get_content_type($meta, $name), + 'content_disposition' => $meta['contentDisposition'] ?? null, + 'file_size' => $this->_get_file_size($meta), + 'width' => $this->_get_width($meta, $wp_meta), + 'height' => $this->_get_height($meta, $wp_meta), + 'stateless_version' => $this->_get_version($meta), + 'storage_class' => $meta['storageClass'] ?? null, + 'file_link' => $this->_get_file_link($name, $meta), + 'self_link' => $this->_get_self_link($meta), + ]; + + $wpdb->insert(ud_stateless_db()->files, $data); + + // Update file sizes data + $sizes = $meta['sizes'] ?? []; + + foreach ($sizes as $size => $size_data) { + $name = $size_data['name']; + + $data = [ + 'post_id' => $item, + 'name' => $name, + 'size_name' => $size, + 'generation' => $this->_get_generation_from_media_link( $size_data['mediaLink'] ), + 'file_size' => $this->_get_file_size($size_data), + 'width' => $this->_get_width($size_data, $wp_meta['sizes'][$size] ?? []), + 'height' => $this->_get_height($size_data, $wp_meta['sizes'][$size] ?? []), + 'file_link' => $this->_get_file_link($name, $meta), + 'self_link' => $this->_get_self_link($size_data), + ]; + + $wpdb->insert(ud_stateless_db()->file_sizes, $data); + } + + // Update file meta data ('fileMd5' for LiteSpeed Cache) + $key = 'fileMd5'; + $md5_data = $meta[$key] ?? []; + + if ( !empty($meta) ) { + $data = [ + 'post_id' => $item, + 'meta_key' => sanitize_key($key), + 'meta_value' => maybe_serialize($md5_data), + ]; + + $wpdb->insert(ud_stateless_db()->file_meta, $data); + } + + $wpdb->query( 'COMMIT' ); + + $this->completed++; + + } catch ( \Throwable $e ) { + $wpdb->query( 'ROLLBACK;' ); + + Helper::log( "Error while processing item $item: " . $e->getMessage() ); + } + + $wpdb->query( 'SET autocommit = 1;' ); + + return false; + } + +} \ No newline at end of file diff --git a/static/scripts/wp-stateless-batch.js b/static/scripts/wp-stateless-batch.js new file mode 100644 index 000000000..fa1745ebf --- /dev/null +++ b/static/scripts/wp-stateless-batch.js @@ -0,0 +1,277 @@ +wpStatelessBatch = { + token: window.wp_stateless_batch.REST_API_TOKEN, + apiRoot: window.wpApiSettings.root + 'wp-stateless/v1/batch/', + interval: null, + + startPolling: function () { + var that = this + this.stopPolling() + this.interval = setInterval(function () { + that.getState() + }, 1000) + }, + + stopPolling: function () { + clearInterval(this.interval) + }, + + updateState: function(state) { + var event = new CustomEvent('wp-stateless-batch-state-updated', { detail: state }) + + document.dispatchEvent(event) + + if (state.is_running) { + this.startPolling() + } else { + this.stopPolling() + } + }, + + processFail: function(error) { + console.log(error) + + var event = new CustomEvent('wp-stateless-batch-error', { detail: error }) + + document.dispatchEvent(event) + }, + + processAction: function(action, payload, callback = null) { + var that = this + + var data = { + action, + ...payload, + } + + jQuery.ajax({ + method: 'POST', + url: that.apiRoot + 'action', + headers: { + 'x-wps-auth': that.token, + 'Content-Type': 'application/json', + }, + dataType: 'json', + data: JSON.stringify(data), + }) + .then(function (response) { + that.updateState(response.data) + }) + .fail(function (error) { + that.processFail(error) + }) + .always(function () { + if (callback) { + callback() + } + }) + }, + + getState: function(data = {}) { + var that = this + + jQuery.ajax({ + method: 'GET', + url: that.apiRoot + 'state', + data, + headers: { + 'x-wps-auth': that.token, + 'Content-Type': 'application/json', + }, + }) + .then(function (response) { + that.updateState(response.data) + }) + .fail(function (error) { + that.processFail(error) + }) + }, + + init: function() { + if ( window.wp_stateless_batch.is_running ) { + this.startPolling() + } + + // Check if we have a batch running on the backend + jQuery(document).on('heartbeat-tick', function (e, data) { + if ( data.hasOwnProperty('stateless-batch-running') && data['stateless-batch-running'] ) { + this.startPolling() + } + }.bind(this)) + } +} + +wpStatelessBatch.init() + +/** + * Manage data updates + */ +function wpMigrations($) { + function getId(element) { + return element.closest('.migration').data('id') + } + + function blockUI() { + $('#stless_status_tab .migration .button').addClass('disabled') + } + + function unblockUI() { + $('#stless_status_tab .migration .button').removeClass('disabled') + } + + // Process state + document.addEventListener('wp-stateless-batch-state-updated', function (e) { + var state = e.detail + + if ( !state.is_migration && !state.hasOwnProperty('migrations') ) { + // If have migrations running on the frontend - we should finalize it + if ( $('#stless_status_tab .migration.can-pause').length || $('#stless_status_tab .migration.can-resume').length ) { + wpStatelessBatch.getState({ + force_migrations: true, + }) + } + + return + } + + if ( !state.hasOwnProperty('migrations') ) { + return + } + + var notify = state.hasOwnProperty('migrations_notify') ? state.migrations_notify : false + + if ( state.is_running ) { + $('#stateless-notice-migrations-required').addClass('hidden') + $('#stateless-notice-migrations-finished').addClass('hidden') + $('#stateless-notice-migrations-running').removeClass('hidden') + } else { + $('#stateless-notice-migrations-running').addClass('hidden') + $('#stateless-notice-migrations-required').addClass('hidden') + $('#stateless-notice-migrations-finished').addClass('hidden') + + if ( notify === 'require' ) { + $('#stateless-notice-migrations-required').removeClass('hidden') + } else if ( notify === 'finished' ) { + $('#stateless-notice-migrations-finished').removeClass('hidden') + } + } + + if ( state.migrations ) { + for (var key of Object.keys(state.migrations)) { + var migration = state.migrations[key] + var migrationElement = $('#stless_status_tab .migration[data-id="' + key + '"]') + + if ( migration.can_start ) { + migrationElement.addClass('can-start') + } else { + migrationElement.removeClass('can-start') + } + + if ( migration.can_pause ) { + migrationElement.addClass('can-pause') + } else { + migrationElement.removeClass('can-pause') + } + + if ( migration.can_resume ) { + migrationElement.addClass('can-resume') + } else { + migrationElement.removeClass('can-resume') + } + + if (migration.message) { + migrationElement.find('.description').html(migration.message) + } + } + } + + // Display progress + if ( state.is_running || state.is_paused ) { + if ( state.hasOwnProperty('total') && state.hasOwnProperty('completed') ) { + var migrationElement = $('#stless_status_tab .migration[data-id="' + state.id + '"]') + + var percent = state.total > 0 ? Math.floor( (state.completed / state.total) * 100 ) + '%' : '' + migrationElement.find('.progress .percent').html(percent) + migrationElement.find('.progress .bar').css('width', percent) + } + } + }) + + // Migration confirmation dialog + $( "#stateless-migration-confirm" ).dialog({ + resizable: false, + height: "auto", + width: 500, + modal: true, + draggable: false, + autoOpen: false, + position: { my: "center", at: "center", of: window }, + open: function(event, ui) { + $(this).closest('.ui-dialog').find('.ui-button').last().addClass('button-primary') + $('body').css('overflow', 'hidden') + }, + close: function(event, ui) { + unblockUI() + $('body').css('overflow', 'auto') + }, + buttons: [ + { + text: stateless_l10n.confirm, + click: function() {}, + }, + { + text: stateless_l10n.cancel, + click: function() { + $( this ).dialog( 'close' ) + }, + }, + ], + }) + + // Migration actions + $('#stless_status_tab .migration .button').click(function (e) { + e.preventDefault() + + if ( $(e.target).hasClass('disabled') ) { + return; + } + + blockUI() + + var id = getId( $(e.target) ) + var action = $(e.target).data('action') + + if ( !id || !action ) { + return + } + + if ( action === 'start' ) { + var migrationElement = $('#stless_status_tab .migration[data-id="' + id + '"]') + var title = migrationElement.find('.title strong').text() + + $( '#stateless-migration-confirm' ).dialog('option', 'title', title) + $( '#stateless-migration-confirm' ).find('strong').text(title) + + $('#stateless-migration-confirm').closest('.ui-dialog').find('.ui-dialog-buttonset .ui-button').first().click(function(e) { + e.preventDefault() + + $( '#stateless-migration-confirm' ).dialog('close') + + wpStatelessBatch.processAction(action, { + id, + is_migration: true, + email: $('input[name="email-notification"]:checked').val(), + }, unblockUI) + }) + + $( "#stateless-migration-confirm" ).dialog('open') + return + } else { + wpStatelessBatch.processAction(action, { + id, + is_migration: true, + }, unblockUI) + } + }.bind(this)) +} + +wpMigrations(jQuery) diff --git a/static/scripts/wp-stateless.js b/static/scripts/wp-stateless.js index e4cdccc66..a3de0b7b2 100644 --- a/static/scripts/wp-stateless.js +++ b/static/scripts/wp-stateless.js @@ -325,6 +325,14 @@ var wpStatelessSettingsApp = { jQuery('#file_url_grp_preview').val(host) }, + showCustomEmail: function () { + if( jQuery('#sm_status_email_type').val() == 'custom' ) { + jQuery('.sm-status-email-address').show() + } else { + jQuery('.sm-status-email-address').hide() + } + }, + // Init application init: function () { this.sm = wp_stateless_settings || {} @@ -375,6 +383,10 @@ var wpStatelessSettingsApp = { jQuery('#sm_root_dir').on('change', this.generatePreviewUrl.bind(this)) jQuery('#custom_domain').on('change', this.generatePreviewUrl.bind(this)) this.generatePreviewUrl() + + // Update root dir depending on folder type + jQuery('#sm_status_email_type').on('change', this.showCustomEmail) + this.showCustomEmail() } }; diff --git a/static/styles/error-notice.css b/static/styles/error-notice.css index b381bc227..d8ebf4382 100644 --- a/static/styles/error-notice.css +++ b/static/styles/error-notice.css @@ -61,6 +61,10 @@ right: 40px; } +.stateless-admin-notice.hidden { + display: none !important; +} + /** Responisve Design: */ diff --git a/static/styles/wp-stateless-status.css b/static/styles/wp-stateless-status.css new file mode 100644 index 000000000..55046c94c --- /dev/null +++ b/static/styles/wp-stateless-status.css @@ -0,0 +1,149 @@ +#stless_status_tab .hndle { + margin-top: 5px; +} + +#stless_status_tab .hndle-notice { + padding: 0 12px; +} + +#stless_status_tab .hndle-notice p { + margin: 0; +} + +#stless_status_tab .hndle-notice ul { + list-style: disc; + padding-left: 20px; +} + +/* Info table */ +#stless_status_tab .stateless-info-table tr { + display: table-row; +} + +#stless_status_tab .stateless-info-table tr:after { + display: none; +} + +#stless_status_tab .stateless-info-table td { + width: 50%; +} + +/* Migrations section (Data updates) */ +#stless_status_tab .migration { + border-top: 1px solid #dcdcde; +} + +#stless_status_tab .migration:last-child { + border-bottom: none; +} + +#stless_status_tab .title { + display: flex; + justify-content: space-between; +} + +#stless_status_tab .title span { + font-style: italic; + color: #646970; +} + +#stless_status_tab .actions .button { + display: none; +} + +#stless_status_tab .can-start .actions .button.start { + display: inline-block; +} + +#stless_status_tab .can-pause .actions .button.pause { + display: inline-block; +} + +#stless_status_tab .can-resume .actions .button.resume { + display: inline-block; +} + +/* Migrations section (Data updates) - progress section */ + +#stless_status_tab .migration .progress-wrap { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + min-height: 20px; +} + +#stless_status_tab .migration .progress-wrap .description { + white-space: nowrap; + margin-right: 20px; +} + +#stless_status_tab .migration .progress { + display: none; + flex: 1; + margin-bottom: 0; + position: relative; + overflow: hidden; + height: 20px; + max-width: 50%; + background-size: 56px 20px; + background-color: #dcdcde; + background-image: repeating-linear-gradient( + -45deg, + transparent, + transparent 10px, + rgba(255,255,255,.7) 10px, + rgba(255,255,255,.7) 20px + ); +} + +#stless_status_tab .migration .progress .bar { + position: absolute; + z-index: 20; + top: 0; + left: 0; + bottom: 0; + height: 100%; + width: 0; + background-color: rgba(34, 113, 177, 0.8); + background-size: 56px 20px; + background-image: repeating-linear-gradient( + -45deg, + transparent, + transparent 10px, + rgba(255,255,255,.4) 10px, + rgba(255,255,255,.4) 20px + ); + transition: all 0.5s ease; +} + +@keyframes stateless-progress { + from {background-position: 0 0;} + to {background-position: 56px 0; } +} + +#stless_status_tab .migration.can-pause .progress, +#stless_status_tab .migration.can-resume .progress { + display: block; +} + +#stless_status_tab .migration.can-pause .progress, +#stless_status_tab .migration.can-pause .progress .bar{ + animation: stateless-progress 3s linear infinite; +} + +#stless_status_tab .migration .progress .percent { + position: absolute; + width: 100%; + height: 100%; + line-height: 20px; + text-align: center; + font-weight: bold; + z-index: 30; +} + +#stateless-migration-confirm label { + display: block; + padding-left: 5px; + margin-top: 5px; +} diff --git a/static/views/error-notice.php b/static/views/error-notice.php index cc1a8a0c1..456ba2cb2 100644 --- a/static/views/error-notice.php +++ b/static/views/error-notice.php @@ -1,4 +1,4 @@ -
+
@@ -9,11 +9,11 @@ endif; ?>
- + - - + + - +
diff --git a/static/views/settings-sections/general.php b/static/views/settings-sections/general.php index dea067d90..1282d4597 100644 --- a/static/views/settings-sections/general.php +++ b/static/views/settings-sections/general.php @@ -99,6 +99,36 @@ domain); ?>

+ +

domain); ?>

+ +
+

+ +

+ +

+ domain); ?> +

+
+ +

+ +

+ diff --git a/static/views/settings_interface.php b/static/views/settings_interface.php index f15a218e8..f5fbe6399 100644 --- a/static/views/settings_interface.php +++ b/static/views/settings_interface.php @@ -12,6 +12,7 @@ domain); ?> + domain); ?>
@@ -40,6 +41,10 @@
+ +
+ +
\ No newline at end of file diff --git a/static/views/status-sections/info.php b/static/views/status-sections/info.php new file mode 100644 index 000000000..f792ce24f --- /dev/null +++ b/static/views/status-sections/info.php @@ -0,0 +1,23 @@ + +
+
+

domain); ?>

+ +
+
+ + + + + + + + + + + + +
+
+
+
\ No newline at end of file diff --git a/static/views/status-sections/migrations.php b/static/views/status-sections/migrations.php new file mode 100644 index 000000000..8f6366dcc --- /dev/null +++ b/static/views/status-sections/migrations.php @@ -0,0 +1,66 @@ + +
+
+

domain); ?>

+
+

domain); ?>

+
    +
  • domain); ?>
  • +
  • domain); ?>
  • +
+
+ +
+
+ $migration ) : ?> +
+

+ description; ?> + + domain); ?> + domain); ?> + domain); ?> + +

+ +
+

message; ?>

+ +
+
+ +
+
+
+ +
+
+
+
+ +get_notification_email(); + $default_email = empty($default_email) ? __('Disabled', ud_get_stateless_media()->domain) : $default_email; + + $current_user = wp_get_current_user(); + $current_email = $current_user->user_email ?? ''; +?> + +
+

Migration.', ud_get_stateless_media()->domain); ?>

+

domain); ?>

+

+ domain); ?> + + + + +

+

domain); ?>

+
diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php index e7ac0e088..14459df7b 100644 --- a/vendor/composer/autoload_classmap.php +++ b/vendor/composer/autoload_classmap.php @@ -34,16 +34,19 @@ 'wpCloud\\StatelessMedia\\Addons' => $baseDir . '/lib/classes/class-addons.php', 'wpCloud\\StatelessMedia\\Ajax' => $baseDir . '/lib/classes/class-ajax.php', 'wpCloud\\StatelessMedia\\AppEngine' => $baseDir . '/lib/classes/class-google-app-engine.php', + 'wpCloud\\StatelessMedia\\Batch\\BatchTask' => $baseDir . '/lib/classes/batch/class-batch-task.php', + 'wpCloud\\StatelessMedia\\Batch\\BatchTaskManager' => $baseDir . '/lib/classes/batch/class-batch-task-manager.php', + 'wpCloud\\StatelessMedia\\Batch\\IBatchTask' => $baseDir . '/lib/classes/batch/interface-batch.php', + 'wpCloud\\StatelessMedia\\Batch\\Migration' => $baseDir . '/lib/classes/batch/class-migration.php', 'wpCloud\\StatelessMedia\\Bootstrap' => $baseDir . '/lib/classes/class-bootstrap.php', - 'wpCloud\\StatelessMedia\\BuddyBoss' => $baseDir . '/lib/classes/compatibility/buddyboss.php', 'wpCloud\\StatelessMedia\\BuddyPress' => $baseDir . '/lib/classes/compatibility/buddypress.php', 'wpCloud\\StatelessMedia\\Compatibility' => $baseDir . '/lib/classes/class-compatibility.php', 'wpCloud\\StatelessMedia\\CompatibilityWooExtraProductOptions' => $baseDir . '/lib/classes/compatibility/woo-extra-product-options.php', + 'wpCloud\\StatelessMedia\\DB' => $baseDir . '/lib/classes/class-db.php', 'wpCloud\\StatelessMedia\\Divi' => $baseDir . '/lib/classes/compatibility/divi.php', 'wpCloud\\StatelessMedia\\DynamicImageSupport' => $baseDir . '/lib/classes/class-dynamic-image-support.php', 'wpCloud\\StatelessMedia\\EDDDownloadMethod' => $baseDir . '/lib/classes/compatibility/easy-digital-downloads.php', 'wpCloud\\StatelessMedia\\EWWW' => $baseDir . '/lib/classes/compatibility/ewww.php', - 'wpCloud\\StatelessMedia\\Elementor' => $baseDir . '/lib/classes/compatibility/elementor.php', 'wpCloud\\StatelessMedia\\Errors' => $baseDir . '/lib/classes/class-errors.php', 'wpCloud\\StatelessMedia\\FatalException' => $baseDir . '/lib/classes/exception-fatal.php', 'wpCloud\\StatelessMedia\\GS_Client' => $baseDir . '/lib/classes/class-gs-client.php', @@ -54,6 +57,7 @@ 'wpCloud\\StatelessMedia\\LSCacheWP' => $baseDir . '/lib/classes/compatibility/lite-speed-cache.php', 'wpCloud\\StatelessMedia\\LearnDash' => $baseDir . '/lib/classes/compatibility/learn-dash.php', 'wpCloud\\StatelessMedia\\Logger' => $baseDir . '/lib/classes/class-logger.php', + 'wpCloud\\StatelessMedia\\Migrator' => $baseDir . '/lib/classes/class-migrator.php', 'wpCloud\\StatelessMedia\\Module' => $baseDir . '/lib/classes/class-module.php', 'wpCloud\\StatelessMedia\\Polylang' => $baseDir . '/lib/classes/compatibility/polylang-pro.php', 'wpCloud\\StatelessMedia\\SOCSS' => $baseDir . '/lib/classes/compatibility/siteorigin-css.php', @@ -62,6 +66,9 @@ 'wpCloud\\StatelessMedia\\ShortPixel' => $baseDir . '/lib/classes/compatibility/shortpixel.php', 'wpCloud\\StatelessMedia\\SimpleLocalAvatars' => $baseDir . '/lib/classes/compatibility/simple-local-avatars.php', 'wpCloud\\StatelessMedia\\Singleton' => $baseDir . '/lib/classes/trait-singleton.php', + 'wpCloud\\StatelessMedia\\Status' => $baseDir . '/lib/classes/class-status.php', + 'wpCloud\\StatelessMedia\\Status\\Info' => $baseDir . '/lib/classes/status/class-info.php', + 'wpCloud\\StatelessMedia\\Status\\Migrations' => $baseDir . '/lib/classes/status/class-migrations.php', 'wpCloud\\StatelessMedia\\StreamWrapper' => $baseDir . '/lib/classes/class-gs-stream-wrapper.php', 'wpCloud\\StatelessMedia\\SyncNonMedia' => $baseDir . '/lib/classes/class-sync-non-media.php', 'wpCloud\\StatelessMedia\\Sync\\BackgroundSync' => $baseDir . '/lib/classes/sync/class-background-sync.php', diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index f0b0b5ddf..0840569c9 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -75,16 +75,19 @@ class ComposerStaticInitc59d002476a452800baaf79c430753cb 'wpCloud\\StatelessMedia\\Addons' => __DIR__ . '/../..' . '/lib/classes/class-addons.php', 'wpCloud\\StatelessMedia\\Ajax' => __DIR__ . '/../..' . '/lib/classes/class-ajax.php', 'wpCloud\\StatelessMedia\\AppEngine' => __DIR__ . '/../..' . '/lib/classes/class-google-app-engine.php', + 'wpCloud\\StatelessMedia\\Batch\\BatchTask' => __DIR__ . '/../..' . '/lib/classes/batch/class-batch-task.php', + 'wpCloud\\StatelessMedia\\Batch\\BatchTaskManager' => __DIR__ . '/../..' . '/lib/classes/batch/class-batch-task-manager.php', + 'wpCloud\\StatelessMedia\\Batch\\IBatchTask' => __DIR__ . '/../..' . '/lib/classes/batch/interface-batch.php', + 'wpCloud\\StatelessMedia\\Batch\\Migration' => __DIR__ . '/../..' . '/lib/classes/batch/class-migration.php', 'wpCloud\\StatelessMedia\\Bootstrap' => __DIR__ . '/../..' . '/lib/classes/class-bootstrap.php', - 'wpCloud\\StatelessMedia\\BuddyBoss' => __DIR__ . '/../..' . '/lib/classes/compatibility/buddyboss.php', 'wpCloud\\StatelessMedia\\BuddyPress' => __DIR__ . '/../..' . '/lib/classes/compatibility/buddypress.php', 'wpCloud\\StatelessMedia\\Compatibility' => __DIR__ . '/../..' . '/lib/classes/class-compatibility.php', 'wpCloud\\StatelessMedia\\CompatibilityWooExtraProductOptions' => __DIR__ . '/../..' . '/lib/classes/compatibility/woo-extra-product-options.php', + 'wpCloud\\StatelessMedia\\DB' => __DIR__ . '/../..' . '/lib/classes/class-db.php', 'wpCloud\\StatelessMedia\\Divi' => __DIR__ . '/../..' . '/lib/classes/compatibility/divi.php', 'wpCloud\\StatelessMedia\\DynamicImageSupport' => __DIR__ . '/../..' . '/lib/classes/class-dynamic-image-support.php', 'wpCloud\\StatelessMedia\\EDDDownloadMethod' => __DIR__ . '/../..' . '/lib/classes/compatibility/easy-digital-downloads.php', 'wpCloud\\StatelessMedia\\EWWW' => __DIR__ . '/../..' . '/lib/classes/compatibility/ewww.php', - 'wpCloud\\StatelessMedia\\Elementor' => __DIR__ . '/../..' . '/lib/classes/compatibility/elementor.php', 'wpCloud\\StatelessMedia\\Errors' => __DIR__ . '/../..' . '/lib/classes/class-errors.php', 'wpCloud\\StatelessMedia\\FatalException' => __DIR__ . '/../..' . '/lib/classes/exception-fatal.php', 'wpCloud\\StatelessMedia\\GS_Client' => __DIR__ . '/../..' . '/lib/classes/class-gs-client.php', @@ -95,6 +98,7 @@ class ComposerStaticInitc59d002476a452800baaf79c430753cb 'wpCloud\\StatelessMedia\\LSCacheWP' => __DIR__ . '/../..' . '/lib/classes/compatibility/lite-speed-cache.php', 'wpCloud\\StatelessMedia\\LearnDash' => __DIR__ . '/../..' . '/lib/classes/compatibility/learn-dash.php', 'wpCloud\\StatelessMedia\\Logger' => __DIR__ . '/../..' . '/lib/classes/class-logger.php', + 'wpCloud\\StatelessMedia\\Migrator' => __DIR__ . '/../..' . '/lib/classes/class-migrator.php', 'wpCloud\\StatelessMedia\\Module' => __DIR__ . '/../..' . '/lib/classes/class-module.php', 'wpCloud\\StatelessMedia\\Polylang' => __DIR__ . '/../..' . '/lib/classes/compatibility/polylang-pro.php', 'wpCloud\\StatelessMedia\\SOCSS' => __DIR__ . '/../..' . '/lib/classes/compatibility/siteorigin-css.php', @@ -103,6 +107,9 @@ class ComposerStaticInitc59d002476a452800baaf79c430753cb 'wpCloud\\StatelessMedia\\ShortPixel' => __DIR__ . '/../..' . '/lib/classes/compatibility/shortpixel.php', 'wpCloud\\StatelessMedia\\SimpleLocalAvatars' => __DIR__ . '/../..' . '/lib/classes/compatibility/simple-local-avatars.php', 'wpCloud\\StatelessMedia\\Singleton' => __DIR__ . '/../..' . '/lib/classes/trait-singleton.php', + 'wpCloud\\StatelessMedia\\Status' => __DIR__ . '/../..' . '/lib/classes/class-status.php', + 'wpCloud\\StatelessMedia\\Status\\Info' => __DIR__ . '/../..' . '/lib/classes/status/class-info.php', + 'wpCloud\\StatelessMedia\\Status\\Migrations' => __DIR__ . '/../..' . '/lib/classes/status/class-migrations.php', 'wpCloud\\StatelessMedia\\StreamWrapper' => __DIR__ . '/../..' . '/lib/classes/class-gs-stream-wrapper.php', 'wpCloud\\StatelessMedia\\SyncNonMedia' => __DIR__ . '/../..' . '/lib/classes/class-sync-non-media.php', 'wpCloud\\StatelessMedia\\Sync\\BackgroundSync' => __DIR__ . '/../..' . '/lib/classes/sync/class-background-sync.php', diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index c2fc7d4c3..b13ab383b 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -264,16 +264,16 @@ }, { "name": "udx/lib-ud-api-client", - "version": "1.2.3", - "version_normalized": "1.2.3.0", + "version": "1.2.4", + "version_normalized": "1.2.4.0", "source": { "type": "git", "url": "git@github.com:udx/lib-ud-api-client", - "reference": "1.2.3" + "reference": "1.2.4" }, "dist": { "type": "zip", - "url": "https://github.com/udx/lib-ud-api-client/archive/1.2.3.zip" + "url": "https://github.com/udx/lib-ud-api-client/archive/1.2.4.zip" }, "require": { "php": ">=5.3" @@ -310,16 +310,16 @@ }, { "name": "udx/lib-wp-bootstrap", - "version": "1.3.2", - "version_normalized": "1.3.2.0", + "version": "1.3.3", + "version_normalized": "1.3.3.0", "source": { "type": "git", "url": "git@github.com:udx/lib-wp-bootstrap", - "reference": "1.3.2" + "reference": "1.3.3" }, "dist": { "type": "zip", - "url": "https://github.com/udx/lib-wp-bootstrap/archive/1.3.2.zip" + "url": "https://github.com/udx/lib-wp-bootstrap/archive/1.3.3.zip" }, "require": { "php": ">=5.3" diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index 04a94e91c..16d4de80c 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'wpcloud/wp-stateless', 'pretty_version' => 'dev-latest', 'version' => 'dev-latest', - 'reference' => '4485e93b09271c7d1d632d20406de711e4d8b391', + 'reference' => '15d050d833585ee8566b7bcdd24752cbd00bb5e7', 'type' => 'wordpress-plugin', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -50,18 +50,18 @@ ), ), 'udx/lib-ud-api-client' => array( - 'pretty_version' => '1.2.3', - 'version' => '1.2.3.0', - 'reference' => '1.2.3', + 'pretty_version' => '1.2.4', + 'version' => '1.2.4.0', + 'reference' => '1.2.4', 'type' => 'library', 'install_path' => __DIR__ . '/../udx/lib-ud-api-client', 'aliases' => array(), 'dev_requirement' => false, ), 'udx/lib-wp-bootstrap' => array( - 'pretty_version' => '1.3.2', - 'version' => '1.3.2.0', - 'reference' => '1.3.2', + 'pretty_version' => '1.3.3', + 'version' => '1.3.3.0', + 'reference' => '1.3.3', 'type' => 'library', 'install_path' => __DIR__ . '/../udx/lib-wp-bootstrap', 'aliases' => array(), @@ -70,7 +70,7 @@ 'wpcloud/wp-stateless' => array( 'pretty_version' => 'dev-latest', 'version' => 'dev-latest', - 'reference' => '4485e93b09271c7d1d632d20406de711e4d8b391', + 'reference' => '15d050d833585ee8566b7bcdd24752cbd00bb5e7', 'type' => 'wordpress-plugin', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), diff --git a/vendor/udx/lib-ud-api-client/changes.md b/vendor/udx/lib-ud-api-client/changes.md index 24e6a33db..8faaf0d15 100644 --- a/vendor/udx/lib-ud-api-client/changes.md +++ b/vendor/udx/lib-ud-api-client/changes.md @@ -1,3 +1,7 @@ +### 1.2.4 + +* Require user to be logged in while dismissing Admin Panel notices. + ### 1.2.3 * Improve security while processing AJAX requests in Admin Panel. diff --git a/vendor/udx/lib-ud-api-client/gruntfile.js b/vendor/udx/lib-ud-api-client/gruntfile.js index 904ab6b69..4deb0ebf1 100644 --- a/vendor/udx/lib-ud-api-client/gruntfile.js +++ b/vendor/udx/lib-ud-api-client/gruntfile.js @@ -2,7 +2,7 @@ * Build Plugin. * * @author potanin@UD - * @version 1.2.3 + * @version 1.2.4 * @param grunt */ module.exports = function( grunt ) { diff --git a/vendor/udx/lib-ud-api-client/lib/classes/class-bootstrap.php b/vendor/udx/lib-ud-api-client/lib/classes/class-bootstrap.php index 21af6e459..ced9fdeb5 100644 --- a/vendor/udx/lib-ud-api-client/lib/classes/class-bootstrap.php +++ b/vendor/udx/lib-ud-api-client/lib/classes/class-bootstrap.php @@ -18,7 +18,7 @@ class Bootstrap extends Scaffold { /** * */ - public static $version = '1.2.3'; + public static $version = '1.2.4'; /** * diff --git a/vendor/udx/lib-ud-api-client/lib/classes/class-update-checker.php b/vendor/udx/lib-ud-api-client/lib/classes/class-update-checker.php index d39c4d57a..8f96ebeff 100644 --- a/vendor/udx/lib-ud-api-client/lib/classes/class-update-checker.php +++ b/vendor/udx/lib-ud-api-client/lib/classes/class-update-checker.php @@ -545,6 +545,10 @@ public function check_dismiss_time( $time = '' ) { public function dismiss_notices(){ check_ajax_referer('ud_api_dismiss'); + if ( !is_user_logged_in() ) { + wp_send_json_error( array( 'error' => __( 'You are not allowed to do this action.', $this->text_domain ) ) ); + } + $response = array( 'success' => '0', 'error' => __( 'There was an error in request.', $this->text_domain ), diff --git a/vendor/udx/lib-ud-api-client/package.json b/vendor/udx/lib-ud-api-client/package.json index d7eb558e6..938163b99 100644 --- a/vendor/udx/lib-ud-api-client/package.json +++ b/vendor/udx/lib-ud-api-client/package.json @@ -1,6 +1,6 @@ { "name": "lib-ud-api-client", - "version": "1.2.3", + "version": "1.2.4", "description": "UD Client for WooCommerce API Manager", "repository": { "type": "git", diff --git a/vendor/udx/lib-wp-bootstrap/changes.md b/vendor/udx/lib-wp-bootstrap/changes.md index d4d30e312..29e76ae36 100644 --- a/vendor/udx/lib-wp-bootstrap/changes.md +++ b/vendor/udx/lib-wp-bootstrap/changes.md @@ -1,3 +1,7 @@ +### 1.3.3 + +* Require user to be logged in while dismissing Admin Panel notices. + ### 1.3.2 * Improve security while processing AJAX requests in Admin Panel. diff --git a/vendor/udx/lib-wp-bootstrap/lib/classes/class-errors.php b/vendor/udx/lib-wp-bootstrap/lib/classes/class-errors.php index 2d78ff9a0..d3945f5e2 100644 --- a/vendor/udx/lib-wp-bootstrap/lib/classes/class-errors.php +++ b/vendor/udx/lib-wp-bootstrap/lib/classes/class-errors.php @@ -234,6 +234,10 @@ public function admin_notices() { public function dismiss_notices() { check_ajax_referer('ud_dismiss'); + if ( !is_user_logged_in() ) { + wp_send_json_error( array( 'error' => __( 'You are not allowed to do this action.', $this->domain ) ) ); + } + $response = array( 'success' => '0', 'error' => __( 'There was an error in request.', $this->domain ), diff --git a/wp-stateless-media.php b/wp-stateless-media.php index 7fde37c46..d389290db 100644 --- a/wp-stateless-media.php +++ b/wp-stateless-media.php @@ -4,10 +4,11 @@ * Plugin URI: https://stateless.udx.io/ * Description: Upload and serve your WordPress media files from Google Cloud Storage. * Author: UDX - * Version: 3.4.1 + * Version: 4.0.0-RC.1 * Text Domain: stateless-media - * Author URI: https://www.udx.io - * + * Author URI: https://udx.io + * License: GPLv2 or later + * * Copyright 2012 - 2024 UDX ( email: info@udx.io ) * */ @@ -30,6 +31,21 @@ function ud_get_stateless_media( $key = false, $default = null ) { } +if( !function_exists( 'ud_stateless_db' ) ) { + + /** + * Returns Stateless Media Database Object instance + * + * @author Usability Dynamics, Inc. + * @since 4.0.0 + * @return \wpCloud\StatelessMedia\DB + */ + function ud_stateless_db() { + return \wpCloud\StatelessMedia\DB::instance(); + } + +} + if( !function_exists( 'ud_check_stateless_media' ) ) { /** * Determines if plugin can be initialized. @@ -100,4 +116,5 @@ function ud_stateless_media_message() { if( ud_check_stateless_media() ) { //** Initialize. */ ud_get_stateless_media(); + ud_stateless_db(); }