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 = '
' . implode( ' ', $errors ) . ' ';
$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); ?>
+
+
+
+
+
+ status_email_type, '' ); ?>>
+
+ status_email_type, 'false' ); ?>>domain); ?>
+ status_email_type, 'true' ); ?>>domain); ?>
+ status_email_type, 'custom' ); ?>>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 @@
+
+
\ 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 @@
+
+
+
+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), $current_email ); ?>
+
+
+
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();
}