first
This commit is contained in:
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "shortpixel/replacer",
|
||||
"description": "Content Replacer",
|
||||
"version": 1.1,
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Bas",
|
||||
"email": "bas@weblogmechanic.com"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "dev",
|
||||
"require": {},
|
||||
"autoload": {
|
||||
"psr-4": { "ShortPixel\\Replacer\\" : "src" }
|
||||
}
|
||||
}
|
@ -0,0 +1,178 @@
|
||||
<?php
|
||||
namespace ShortPixel\Replacer\Libraries\Unserialize;
|
||||
|
||||
|
||||
/**
|
||||
* Worker implementation for identifying and skipping false-positives
|
||||
* not to be substituted - like nested serializations in string literals.
|
||||
*
|
||||
* @internal This class should only be used by \Brumann\Polyfill\Unserialize
|
||||
*/
|
||||
final class DisallowedClassesSubstitutor
|
||||
{
|
||||
const PATTERN_STRING = '#s:(\d+):(")#';
|
||||
const PATTERN_OBJECT = '#(^|;)O:\d+:"([^"]*)":(\d+):\{#';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $serialized;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $allowedClasses;
|
||||
|
||||
/**
|
||||
* Each array item consists of `[<offset-start>, <offset-end>]` and
|
||||
* marks start and end positions of items to be ignored.
|
||||
*
|
||||
* @var array[]
|
||||
*/
|
||||
private $ignoreItems = array();
|
||||
|
||||
/**
|
||||
* @param string $serialized
|
||||
* @param string[] $allowedClasses
|
||||
*/
|
||||
public function __construct($serialized, array $allowedClasses)
|
||||
{
|
||||
$this->serialized = $serialized;
|
||||
$this->allowedClasses = $allowedClasses;
|
||||
|
||||
$this->buildIgnoreItems();
|
||||
$this->substituteObjects();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getSubstitutedSerialized()
|
||||
{
|
||||
return $this->serialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies items to be ignored - like nested serializations in string literals.
|
||||
*/
|
||||
private function buildIgnoreItems()
|
||||
{
|
||||
$offset = 0;
|
||||
while (preg_match(self::PATTERN_STRING, $this->serialized, $matches, PREG_OFFSET_CAPTURE, $offset)) {
|
||||
$length = (int)$matches[1][0]; // given length in serialized data (e.g. `s:123:"` --> 123)
|
||||
$start = $matches[2][1]; // offset position of quote character
|
||||
$end = $start + $length + 1;
|
||||
$offset = $end + 1;
|
||||
|
||||
// serialized string nested in outer serialized string
|
||||
if ($this->ignore($start, $end)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->ignoreItems[] = array($start, $end);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitutes disallowed object class names and respects items to be ignored.
|
||||
*/
|
||||
private function substituteObjects()
|
||||
{
|
||||
$offset = 0;
|
||||
while (preg_match(self::PATTERN_OBJECT, $this->serialized, $matches, PREG_OFFSET_CAPTURE, $offset)) {
|
||||
$completeMatch = (string)$matches[0][0];
|
||||
$completeLength = strlen($completeMatch);
|
||||
$start = $matches[0][1];
|
||||
$end = $start + $completeLength;
|
||||
$leftBorder = (string)$matches[1][0];
|
||||
$className = (string)$matches[2][0];
|
||||
$objectSize = (int)$matches[3][0];
|
||||
$offset = $end + 1;
|
||||
|
||||
// class name is actually allowed - skip this item
|
||||
if (in_array($className, $this->allowedClasses, true)) {
|
||||
continue;
|
||||
}
|
||||
// serialized object nested in outer serialized string
|
||||
if ($this->ignore($start, $end)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$incompleteItem = $this->sanitizeItem($className, $leftBorder, $objectSize);
|
||||
$incompleteItemLength = strlen($incompleteItem);
|
||||
$offset = $start + $incompleteItemLength + 1;
|
||||
|
||||
$this->replace($incompleteItem, $start, $end);
|
||||
$this->shift($end, $incompleteItemLength - $completeLength);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces sanitized object class names in serialized data.
|
||||
*
|
||||
* @param string $replacement Sanitized object data
|
||||
* @param int $start Start offset in serialized data
|
||||
* @param int $end End offset in serialized data
|
||||
*/
|
||||
private function replace($replacement, $start, $end)
|
||||
{
|
||||
$this->serialized = substr($this->serialized, 0, $start)
|
||||
. $replacement . substr($this->serialized, $end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether given offset positions should be ignored.
|
||||
*
|
||||
* @param int $start
|
||||
* @param int $end
|
||||
* @return bool
|
||||
*/
|
||||
private function ignore($start, $end)
|
||||
{
|
||||
foreach ($this->ignoreItems as $ignoreItem) {
|
||||
if ($ignoreItem[0] <= $start && $ignoreItem[1] >= $end) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shifts offset positions of ignore items by `$size`.
|
||||
* This is necessary whenever object class names have been
|
||||
* substituted which have a different length than before.
|
||||
*
|
||||
* @param int $offset
|
||||
* @param int $size
|
||||
*/
|
||||
private function shift($offset, $size)
|
||||
{
|
||||
foreach ($this->ignoreItems as &$ignoreItem) {
|
||||
// only focus on items starting after given offset
|
||||
if ($ignoreItem[0] < $offset) {
|
||||
continue;
|
||||
}
|
||||
$ignoreItem[0] += $size;
|
||||
$ignoreItem[1] += $size;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes object class item.
|
||||
*
|
||||
* @param string $className
|
||||
* @param int $leftBorder
|
||||
* @param int $objectSize
|
||||
* @return string
|
||||
*/
|
||||
private function sanitizeItem($className, $leftBorder, $objectSize)
|
||||
{
|
||||
return sprintf(
|
||||
'%sO:22:"__PHP_Incomplete_Class":%d:{s:27:"__PHP_Incomplete_Class_Name";%s',
|
||||
$leftBorder,
|
||||
$objectSize + 1, // size of object + 1 for added string
|
||||
\serialize($className)
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016-2019 Denis Brumann
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,153 @@
|
||||
Polyfill unserialize [](https://travis-ci.org/dbrumann/polyfill-unserialize)
|
||||
===
|
||||
|
||||
Backports unserialize options introduced in PHP 7.0 to older PHP versions.
|
||||
This was originally designed as a Proof of Concept for Symfony Issue
|
||||
[#21090](https://github.com/symfony/symfony/pull/21090).
|
||||
|
||||
You can use this package in projects that rely on PHP versions older than
|
||||
PHP 7.0. In case you are using PHP 7.0+ the original `unserialize()` will be
|
||||
used instead.
|
||||
|
||||
From the [documentation](https://secure.php.net/manual/en/function.unserialize.php):
|
||||
|
||||
> **Warning**
|
||||
>
|
||||
> Do not pass untrusted user input to unserialize() regardless of the options
|
||||
> value of allowed_classes. Unserialization can result in code being loaded and
|
||||
> executed due to object instantiation and autoloading, and a malicious user
|
||||
> may be able to exploit this. Use a safe, standard data interchange format
|
||||
> such as JSON (via json_decode() and json_encode()) if you need to pass
|
||||
> serialized data to the user.
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
- PHP 5.3+
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
You can install this package via composer:
|
||||
|
||||
```bash
|
||||
composer require brumann/polyfill-unserialize "^2.0"
|
||||
```
|
||||
|
||||
Older versions
|
||||
--------------
|
||||
|
||||
You can find the most recent 1.x versions in the branch with the same name:
|
||||
|
||||
* [dbrumann/polyfill-unserialize/tree/1.x](https://github.com/dbrumann/polyfill-unserialize/tree/1.x)
|
||||
|
||||
Upgrading
|
||||
---------
|
||||
|
||||
Upgrading from 1.x to 2.0 should be seamless and require no changes to code
|
||||
using the library. There are no changes to the public API, i.e. the names for
|
||||
classes, methods and arguments as well as argument order and types remain the
|
||||
same. Version 2.x uses a completely different approach for substituting
|
||||
disallowed classes, which is why we chose to use a new major release to prevent
|
||||
issues from unknown side effects in existing installations.
|
||||
|
||||
Known Issues
|
||||
------------
|
||||
|
||||
There is a mismatch in behavior when `allowed_classes` in `$options` is not
|
||||
of the correct type (array or boolean). PHP 7.0 will not issue a warning that
|
||||
an invalid type was provided. This library will trigger a warning, similar to
|
||||
the one PHP 7.1+ will raise and then continue, assuming `false` to make sure
|
||||
no classes are deserialized by accident.
|
||||
|
||||
Tests
|
||||
-----
|
||||
|
||||
You can run the test suite using PHPUnit. It is intentionally not bundled as
|
||||
dev dependency to make sure this package has the lowest restrictions on the
|
||||
implementing system as possible.
|
||||
|
||||
Please read the [PHPUnit Manual](https://phpunit.de/manual/current/en/installation.html)
|
||||
for information how to install it on your system.
|
||||
|
||||
Please make sure to pick a compatible version. If you use PHP 5.6 you should
|
||||
use PHPUnit 5.7.27 and for older PHP versions you should use PHPUnit 4.8.36.
|
||||
Older versions of PHPUnit might not support namespaces, meaning they will not
|
||||
work with the tests. Newer versions only support PHP 7.0+, where this library
|
||||
is not needed anymore.
|
||||
|
||||
You can run the test suite as follows:
|
||||
|
||||
```bash
|
||||
phpunit -c phpunit.xml.dist tests/
|
||||
```
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
This package is considered feature complete. As such I will likely not update
|
||||
it unless there are security issues.
|
||||
|
||||
Should you find any bugs or have questions, feel free to submit an Issue or a
|
||||
Pull Request on GitHub.
|
||||
|
||||
Development setup
|
||||
-----------------
|
||||
|
||||
This library contains a docker setup for development purposes. This allows
|
||||
running the code on an older PHP version without having to install it locally.
|
||||
|
||||
You can use the setup as follows:
|
||||
|
||||
1. Go into the project directory
|
||||
|
||||
1. Build the docker image
|
||||
|
||||
```
|
||||
docker build -t polyfill-unserialize .
|
||||
```
|
||||
|
||||
This will download a debian/jessie container with PHP 5.6 installed. Then
|
||||
it will download an appropriate version of phpunit for this PHP version.
|
||||
It will also download composer. It will set the working directory to `/opt/app`.
|
||||
The resulting image is tagged as `polyfill-unserialize`, which is the name
|
||||
we will refer to, when running the container.
|
||||
|
||||
1. You can then run a container based on the image, which will run your tests
|
||||
|
||||
```
|
||||
docker run -it --rm --name polyfill-unserialize-dev -v "$PWD":/opt/app polyfill-unserialize
|
||||
```
|
||||
|
||||
This will run a docker container based on our previously built image.
|
||||
The container will automatically be removed after phpunit finishes.
|
||||
We name the image `polyfill-unserialize-dev`. This makes sure only one
|
||||
instance is running and that we can easily identify a running container by
|
||||
its name, e.g. in order to remove it manually.
|
||||
We mount our current directory into the container's working directory.
|
||||
This ensures that tests run on our current project's state.
|
||||
|
||||
You can repeat the final step as often as you like in order to run the tests.
|
||||
The output should look something like this:
|
||||
|
||||
```bash
|
||||
dbr:polyfill-unserialize/ (improvement/dev_setup*) $ docker run -it --rm --name polyfill-unserialize-dev -v "$PWD":/opt/app polyfill-unserialize
|
||||
Loading composer repositories with package information
|
||||
Installing dependencies (including require-dev) from lock file
|
||||
Nothing to install or update
|
||||
Generating autoload files
|
||||
PHPUnit 5.7.27 by Sebastian Bergmann and contributors.
|
||||
|
||||
...................... 22 / 22 (100%)
|
||||
|
||||
Time: 167 ms, Memory: 13.25MB
|
||||
|
||||
OK (22 tests, 31 assertions)
|
||||
```
|
||||
|
||||
When you are done working on the project you can free up disk space by removing
|
||||
the initially built image:
|
||||
|
||||
```
|
||||
docker image rm polyfill-unserialize
|
||||
```
|
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
namespace ShortPixel\Replacer\Libraries\Unserialize;
|
||||
|
||||
// Taken from : https://github.com/dbrumann/polyfill-unserialize/
|
||||
final class Unserialize
|
||||
{
|
||||
/**
|
||||
* @see https://secure.php.net/manual/en/function.unserialize.php
|
||||
*
|
||||
* @param string $serialized Serialized data
|
||||
* @param array $options Associative array containing options
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function unserialize($serialized, array $options = array())
|
||||
{
|
||||
if (PHP_VERSION_ID >= 70000) {
|
||||
return \unserialize($serialized, $options);
|
||||
}
|
||||
if (!array_key_exists('allowed_classes', $options) || true === $options['allowed_classes']) {
|
||||
return \unserialize($serialized);
|
||||
}
|
||||
$allowedClasses = $options['allowed_classes'];
|
||||
if (false === $allowedClasses) {
|
||||
$allowedClasses = array();
|
||||
}
|
||||
if (!is_array($allowedClasses)) {
|
||||
$allowedClasses = array();
|
||||
trigger_error(
|
||||
'unserialize(): allowed_classes option should be array or boolean',
|
||||
E_USER_WARNING
|
||||
);
|
||||
}
|
||||
|
||||
$worker = new DisallowedClassesSubstitutor($serialized, $allowedClasses);
|
||||
|
||||
return \unserialize($worker->getSubstitutedSerialized());
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
namespace ShortPixel\Replacer\Modules;
|
||||
|
||||
class Elementor
|
||||
{
|
||||
private static $instance;
|
||||
|
||||
protected $queryKey = 'elementor';
|
||||
|
||||
public static function getInstance()
|
||||
{
|
||||
if (is_null(self::$instance))
|
||||
self::$instance = new Elementor();
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if ($this->elementor_is_active()) // elementor is active
|
||||
{
|
||||
add_filter('shortpixel/replacer/custom_replace_query', array($this, 'addElementor'), 10, 4); // custom query for elementor \ // problem
|
||||
// @todo Fix this for SPIO
|
||||
//add_action('enable-media-replace-upload-done', array($this, 'removeCache') );
|
||||
}
|
||||
}
|
||||
|
||||
public function addElementor($items, $base_url, $search_urls, $replace_urls)
|
||||
{
|
||||
$base_url = $this->addSlash($base_url);
|
||||
$el_search_urls = $search_urls; //array_map(array($this, 'addslash'), $search_urls);
|
||||
$el_replace_urls = $replace_urls; //array_map(array($this, 'addslash'), $replace_urls);
|
||||
$items[$this->queryKey] = array('base_url' => $base_url, 'search_urls' => $el_search_urls, 'replace_urls' => $el_replace_urls);
|
||||
return $items;
|
||||
}
|
||||
|
||||
public function addSlash($value)
|
||||
{
|
||||
global $wpdb;
|
||||
$value= ltrim($value, '/'); // for some reason the left / isn't picked up by Mysql.
|
||||
$value= str_replace('/', '\/', $value);
|
||||
$value = $wpdb->esc_like(($value)); //(wp_slash) / str_replace('/', '\/', $value);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
protected function elementor_is_active()
|
||||
{
|
||||
$bool = false;
|
||||
|
||||
if (defined('ELEMENTOR_VERSION'))
|
||||
$bool = true;
|
||||
|
||||
return apply_filters('emr/externals/elementor_is_active', $bool); // manual override
|
||||
}
|
||||
|
||||
public function removeCache()
|
||||
{
|
||||
\Elementor\Plugin::$instance->files_manager->clear_cache();
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
namespace ShortPixel\Replacer\Modules;
|
||||
// Note! This class doubles as integration for both Visual Composer *and* WP Bakery. They both need URLENCODE.
|
||||
class WpBakery
|
||||
{
|
||||
private static $instance;
|
||||
|
||||
protected $queryKey = 'wpbakery';
|
||||
|
||||
public static function getInstance()
|
||||
{
|
||||
if (is_null(self::$instance))
|
||||
self::$instance = new WpBakery();
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if ($this->bakery_is_active()) // elementor is active
|
||||
{
|
||||
add_filter('shortpixel/replacer/custom_replace_query', array($this, 'addURLEncoded'), 10, 4); // custom query for elementor \ // problem
|
||||
}
|
||||
}
|
||||
|
||||
public function addUrlEncoded($items, $base_url, $search_urls, $replace_urls)
|
||||
{
|
||||
$base_url = $this->addEncode($base_url);
|
||||
$el_search_urls = array_map(array($this, 'addEncode'), $search_urls);
|
||||
$el_replace_urls = array_map(array($this, 'addEncode'), $replace_urls);
|
||||
$items[$this->queryKey] = array('base_url' => $base_url, 'search_urls' => $el_search_urls, 'replace_urls' => $el_replace_urls);
|
||||
return $items;
|
||||
}
|
||||
|
||||
public function addEncode($value)
|
||||
{
|
||||
return urlencode($value);
|
||||
}
|
||||
|
||||
protected function bakery_is_active()
|
||||
{
|
||||
$bool = false;
|
||||
|
||||
// did_action -> wpbakery , VCV_version -> detect Visual Composer
|
||||
if (did_action('vc_plugins_loaded') || defined('VCV_VERSION'))
|
||||
$bool = true;
|
||||
|
||||
return apply_filters('emr/externals/urlencode_is_active', $bool); // manual override
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
namespace ShortPixel\Replacer\Modules;
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
|
||||
// Integration to reset indexes of Yoast (used for Og:image) when something is converted.
|
||||
class YoastSeo
|
||||
{
|
||||
|
||||
private $yoastTable;
|
||||
private static $instance;
|
||||
|
||||
public static function getInstance()
|
||||
{
|
||||
if (is_null(self::$instance))
|
||||
self::$instance = new YoastSeo();
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if (true === $this->yoast_is_active()) // elementor is active
|
||||
{
|
||||
global $wpdb;
|
||||
$this->yoastTable = $wpdb->prefix . 'yoast_indexable';
|
||||
|
||||
add_action('shortpixel/replacer/replace_urls', array($this, 'removeIndexes'),10,2);
|
||||
}
|
||||
}
|
||||
|
||||
public function removeIndexes($search_urls, $replace_urls)
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$sql = 'DELETE FROM ' . $this->yoastTable . ' WHERE ';
|
||||
$prepare = array();
|
||||
|
||||
$base = isset($search_urls['base']) ? $search_urls['base'] : null;
|
||||
$file = isset($search_urls['file']) ? $search_urls['file'] : null;
|
||||
|
||||
if (! is_null($base))
|
||||
{
|
||||
$querySQL = $sql . ' twitter_image like %s or open_graph_image like %s ';
|
||||
$querySQL = $wpdb->prepare($querySQL, '%' . $base . '%', '%' . $base . '%');
|
||||
|
||||
$wpdb->query($querySQL);
|
||||
}
|
||||
|
||||
if (! is_null($file))
|
||||
{
|
||||
$querySQL = $sql . ' twitter_image like %s or open_graph_image like %s ';
|
||||
$querySQL = $wpdb->prepare($querySQL, '%' . $file . '%', '%' . $file . '%');
|
||||
|
||||
$wpdb->query($querySQL);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected function yoast_is_active()
|
||||
{
|
||||
if (defined('WPSEO_VERSION'))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,527 @@
|
||||
<?php
|
||||
namespace ShortPixel\Replacer;
|
||||
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
use ShortPixel\Replacer\Libraries\Unserialize\Unserialize;
|
||||
|
||||
/** Module: Replacer.
|
||||
*
|
||||
* - Able to replace across database
|
||||
* - Only replace thumbnails feature dependent on media library
|
||||
* - Support for page builders / strange data
|
||||
*/
|
||||
|
||||
class Replacer
|
||||
{
|
||||
|
||||
protected $source_url;
|
||||
protected $target_url;
|
||||
protected $source_metadata = array();
|
||||
protected $target_metadata = array();
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
//$this->source_url = $source_url;
|
||||
///$this->target_url = $target_url;
|
||||
$this->loadFormats();
|
||||
}
|
||||
|
||||
// Load classes that handle alternative formats that can occur in the metadata / post data.
|
||||
protected function loadFormats()
|
||||
{
|
||||
Modules\Elementor::getInstance();
|
||||
Modules\WpBakery::getInstance();
|
||||
Modules\YoastSeo::getInstance();
|
||||
}
|
||||
|
||||
public function setSource($url)
|
||||
{
|
||||
$this->source_url = $url;
|
||||
}
|
||||
|
||||
public function getSource()
|
||||
{
|
||||
return $this->source_url;
|
||||
}
|
||||
|
||||
public function setTarget($url)
|
||||
{
|
||||
$this->target_url = $url;
|
||||
}
|
||||
|
||||
public function getTarget()
|
||||
{
|
||||
return $this->target_url;
|
||||
}
|
||||
|
||||
public function setSourceMeta($meta)
|
||||
{
|
||||
$this->source_metadata = $meta;
|
||||
}
|
||||
|
||||
public function setTargetMeta($meta)
|
||||
{
|
||||
$this->target_metadata = $meta;
|
||||
}
|
||||
|
||||
public function replace($args = array())
|
||||
{
|
||||
if (is_null($this->source_url) || is_null($this->target_url))
|
||||
{
|
||||
Log::addWarn('Replacer called without source or target ');
|
||||
return false;
|
||||
}
|
||||
$defaults = array(
|
||||
'thumbnails_only' => false,
|
||||
);
|
||||
|
||||
$errors = array();
|
||||
$args = wp_parse_args($args, $defaults);
|
||||
|
||||
// Search-and-replace filename in post database
|
||||
// @todo Check this with scaled images.
|
||||
$base_url = parse_url($this->source_url, PHP_URL_PATH);// emr_get_match_url( $this->source_url);
|
||||
$base_url = str_replace('.' . pathinfo($base_url, PATHINFO_EXTENSION), '', $base_url);
|
||||
|
||||
/** Fail-safe if base_url is a whole directory, don't go search/replace */
|
||||
if (is_dir($base_url))
|
||||
{
|
||||
Log::addError('Search Replace tried to replace to directory - ' . $base_url);
|
||||
$errors[] = __('Fail Safe :: Source Location seems to be a directory.', 'enable-media-replace');
|
||||
return $errors;
|
||||
}
|
||||
|
||||
if (strlen(trim($base_url)) == 0)
|
||||
{
|
||||
Log::addError('Current Base URL emtpy - ' . $base_url);
|
||||
$errors[] = __('Fail Safe :: Source Location returned empty string. Not replacing content','enable-media-replace');
|
||||
return $errors;
|
||||
}
|
||||
|
||||
// get relurls of both source and target.
|
||||
$urls = $this->getRelativeURLS();
|
||||
|
||||
|
||||
if ($args['thumbnails_only'])
|
||||
{
|
||||
foreach($urls as $side => $data)
|
||||
{
|
||||
if (isset($data['base']))
|
||||
{
|
||||
unset($urls[$side]['base']);
|
||||
}
|
||||
if (isset($data['file']))
|
||||
{
|
||||
unset($urls[$side]['file']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$search_urls = $urls['source'];
|
||||
$replace_urls = $urls['target'];
|
||||
|
||||
/* If the replacement is much larger than the source, there can be more thumbnails. This leads to disbalance in the search/replace arrays.
|
||||
Remove those from the equation. If the size doesn't exist in the source, it shouldn't be in use either */
|
||||
foreach($replace_urls as $size => $url)
|
||||
{
|
||||
if (! isset($search_urls[$size]))
|
||||
{
|
||||
Log::addDebug('Dropping size ' . $size . ' - not found in source urls');
|
||||
unset($replace_urls[$size]);
|
||||
}
|
||||
}
|
||||
|
||||
Log::addDebug('Source', $this->source_metadata);
|
||||
Log::addDebug('Target', $this->target_metadata);
|
||||
/* If on the other hand, some sizes are available in source, but not in target, try to replace them with something closeby. */
|
||||
foreach($search_urls as $size => $url)
|
||||
{
|
||||
if (! isset($replace_urls[$size]))
|
||||
{
|
||||
$closest = $this->findNearestSize($size);
|
||||
if ($closest)
|
||||
{
|
||||
$sourceUrl = $search_urls[$size];
|
||||
$baseurl = trailingslashit(str_replace(wp_basename($sourceUrl), '', $sourceUrl));
|
||||
Log::addDebug('Nearest size of source ' . $size . ' for target is ' . $closest);
|
||||
$replace_urls[$size] = $baseurl . $closest;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log::addDebug('Unset size ' . $size . ' - no closest found in source');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* If source and target are the same, remove them from replace. This happens when replacing a file with same name, and +/- same dimensions generated.
|
||||
|
||||
After previous loops, for every search there should be a replace size.
|
||||
*/
|
||||
foreach($search_urls as $size => $url)
|
||||
{
|
||||
$replace_url = isset($replace_urls[$size]) ? $replace_urls[$size] : false;
|
||||
if ($url == $replace_url) // if source and target as the same, no need for replacing.
|
||||
{
|
||||
unset($search_urls[$size]);
|
||||
unset($replace_urls[$size]);
|
||||
}
|
||||
}
|
||||
|
||||
// If the two sides are disbalanced, the str_replace part will cause everything that has an empty replace counterpart to replace it with empty. Unwanted.
|
||||
if (count($search_urls) !== count($replace_urls))
|
||||
{
|
||||
Log::addError('Unbalanced Replace Arrays, aborting', array($search_urls, $replace_urls, count($search_urls), count($replace_urls) ));
|
||||
$errors[] = __('There was an issue with updating your image URLS: Search and replace have different amount of values. Aborting updating thumbnails', 'enable-media-replace');
|
||||
return $errors;
|
||||
}
|
||||
|
||||
Log::addDebug('Doing meta search and replace -', array($search_urls, $replace_urls) );
|
||||
Log::addDebug('Searching with BaseuRL ' . $base_url);
|
||||
|
||||
do_action('shortpixel/replacer/replace_urls', $search_urls, $replace_urls);
|
||||
$updated = 0;
|
||||
|
||||
$updated += $this->doReplaceQuery($base_url, $search_urls, $replace_urls);
|
||||
|
||||
$replaceRuns = apply_filters('shortpixel/replacer/custom_replace_query', array(), $base_url, $search_urls, $replace_urls);
|
||||
Log::addDebug("REPLACE RUNS", $replaceRuns);
|
||||
foreach($replaceRuns as $component => $run)
|
||||
{
|
||||
Log::addDebug('Running additional replace for : '. $component, $run);
|
||||
$updated += $this->doReplaceQuery($run['base_url'], $run['search_urls'], $run['replace_urls']);
|
||||
}
|
||||
|
||||
Log::addDebug("Updated Records : " . $updated);
|
||||
return $updated;
|
||||
}
|
||||
|
||||
private function doReplaceQuery($base_url, $search_urls, $replace_urls)
|
||||
{
|
||||
global $wpdb;
|
||||
/* Search and replace in WP_POSTS */
|
||||
// Removed $wpdb->remove_placeholder_escape from here, not compatible with WP 4.8
|
||||
|
||||
$posts_sql = $wpdb->prepare(
|
||||
"SELECT ID, post_content FROM $wpdb->posts WHERE post_status in ('publish', 'future', 'draft', 'pending', 'private')
|
||||
AND post_content LIKE %s",
|
||||
'%' . $base_url . '%');
|
||||
|
||||
$rs = $wpdb->get_results( $posts_sql, ARRAY_A );
|
||||
$number_of_updates = 0;
|
||||
|
||||
if ( ! empty( $rs ) ) {
|
||||
foreach ( $rs AS $rows ) {
|
||||
$number_of_updates = $number_of_updates + 1;
|
||||
// replace old URLs with new URLs.
|
||||
|
||||
$post_content = $rows["post_content"];
|
||||
$post_id = $rows['ID'];
|
||||
$replaced_content = $this->replaceContent($post_content, $search_urls, $replace_urls, false, true);
|
||||
|
||||
if ($replaced_content !== $post_content)
|
||||
{
|
||||
|
||||
// $result = wp_update_post($post_ar);
|
||||
$sql = 'UPDATE ' . $wpdb->posts . ' SET post_content = %s WHERE ID = %d';
|
||||
$sql = $wpdb->prepare($sql, $replaced_content, $post_id);
|
||||
|
||||
$result = $wpdb->query($sql);
|
||||
|
||||
if ($result === false)
|
||||
{
|
||||
Notice::addError('Something went wrong while replacing' . $result->get_error_message() );
|
||||
Log::addError('WP-Error during post update', $result);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
$number_of_updates += $this->handleMetaData($base_url, $search_urls, $replace_urls);
|
||||
return $number_of_updates;
|
||||
}
|
||||
|
||||
private function handleMetaData($url, $search_urls, $replace_urls)
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$meta_options = apply_filters('shortpixel/replacer/metadata_tables', array('post', 'comment', 'term', 'user'));
|
||||
$number_of_updates = 0;
|
||||
|
||||
foreach($meta_options as $type)
|
||||
{
|
||||
switch($type)
|
||||
{
|
||||
case "post": // special case.
|
||||
$sql = 'SELECT meta_id as id, meta_key, meta_value FROM ' . $wpdb->postmeta . '
|
||||
WHERE post_id in (SELECT ID from '. $wpdb->posts . ' where post_status in ("publish", "future", "draft", "pending", "private") ) AND meta_value like %s';
|
||||
$type = 'post';
|
||||
|
||||
$update_sql = ' UPDATE ' . $wpdb->postmeta . ' SET meta_value = %s WHERE meta_id = %d';
|
||||
break;
|
||||
default:
|
||||
$table = $wpdb->{$type . 'meta'}; // termmeta, commentmeta etc
|
||||
|
||||
$meta_id = 'meta_id';
|
||||
if ($type == 'user')
|
||||
$meta_id = 'umeta_id';
|
||||
|
||||
$sql = 'SELECT ' . $meta_id . ' as id, meta_value FROM ' . $table . '
|
||||
WHERE meta_value like %s';
|
||||
|
||||
$update_sql = " UPDATE $table set meta_value = %s WHERE $meta_id = %d ";
|
||||
break;
|
||||
}
|
||||
|
||||
$sql = $wpdb->prepare($sql, '%' . $url . '%');
|
||||
|
||||
// This is a desparate solution. Can't find anyway for wpdb->prepare not the add extra slashes to the query, which messes up the query.
|
||||
// $postmeta_sql = str_replace('[JSON_URL]', $json_url, $postmeta_sql);
|
||||
$rsmeta = $wpdb->get_results($sql, ARRAY_A);
|
||||
|
||||
if (! empty($rsmeta))
|
||||
{
|
||||
foreach ($rsmeta as $row)
|
||||
{
|
||||
$number_of_updates++;
|
||||
$content = $row['meta_value'];
|
||||
|
||||
|
||||
$id = $row['id'];
|
||||
|
||||
$content = $this->replaceContent($content, $search_urls, $replace_urls); //str_replace($search_urls, $replace_urls, $content);
|
||||
|
||||
$prepared_sql = $wpdb->prepare($update_sql, $content, $id);
|
||||
|
||||
Log::addDebug('Update Meta SQl' . $prepared_sql);
|
||||
$result = $wpdb->query($prepared_sql);
|
||||
|
||||
}
|
||||
}
|
||||
} // foreach
|
||||
|
||||
return $number_of_updates;
|
||||
} // function
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Replaces Content across several levels of possible data
|
||||
* @param $content String The Content to replace
|
||||
* @param $search String Search string
|
||||
* @param $replace String Replacement String
|
||||
* @param $in_deep Boolean. This is use to prevent serialization of sublevels. Only pass back serialized from top.
|
||||
* @param $strict_check Boolean . If true, remove all classes from serialization check and fail. This should be done on post_content, not on metadata.
|
||||
*/
|
||||
private function replaceContent($content, $search, $replace, $in_deep = false, $strict_check = false)
|
||||
{
|
||||
//$is_serial = false;
|
||||
if ( true === is_serialized($content))
|
||||
{
|
||||
$serialized_content = $content; // use to return content back if incomplete classes are found, prevent destroying the original information
|
||||
|
||||
if (true === $strict_check)
|
||||
{
|
||||
$args = array('allowed_classes' => false);
|
||||
}
|
||||
else
|
||||
{
|
||||
$args = array('allowed_classes' => true);
|
||||
}
|
||||
|
||||
$content = Unserialize::unserialize($content, $args);
|
||||
// bail directly on incomplete classes. In < PHP 7.2 is_object is false on incomplete objects!
|
||||
if (true === $this->checkIncomplete($content))
|
||||
{
|
||||
return $serialized_content;
|
||||
}
|
||||
}
|
||||
|
||||
$isJson = $this->isJSON($content);
|
||||
|
||||
if ($isJson)
|
||||
{
|
||||
$content = json_decode($content);
|
||||
Log::addDebug('JSon Content', $content);
|
||||
}
|
||||
|
||||
if (is_string($content)) // let's check the normal one first.
|
||||
{
|
||||
$content = apply_filters('shortpixel/replacer/content', $content, $search, $replace);
|
||||
|
||||
$content = str_replace($search, $replace, $content);
|
||||
}
|
||||
elseif (is_wp_error($content)) // seen this.
|
||||
{
|
||||
//return $content; // do nothing.
|
||||
}
|
||||
elseif (is_array($content) ) // array metadata and such.
|
||||
{
|
||||
foreach($content as $index => $value)
|
||||
{
|
||||
$content[$index] = $this->replaceContent($value, $search, $replace, true); //str_replace($value, $search, $replace);
|
||||
if (is_string($index)) // If the key is the URL (sigh)
|
||||
{
|
||||
$index_replaced = $this->replaceContent($index, $search,$replace, true);
|
||||
if ($index_replaced !== $index)
|
||||
$content = $this->change_key($content, array($index => $index_replaced));
|
||||
}
|
||||
}
|
||||
}
|
||||
elseif(is_object($content)) // metadata objects, they exist.
|
||||
{
|
||||
// bail directly on incomplete classes.
|
||||
if (true === $this->checkIncomplete($content))
|
||||
{
|
||||
// if it was serialized, return the original as not to corrupt data.
|
||||
if (isset($serialized_content))
|
||||
{
|
||||
return $serialized_content;
|
||||
}
|
||||
else { // else just return the content.
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
foreach($content as $key => $value)
|
||||
{
|
||||
$content->{$key} = $this->replaceContent($value, $search, $replace, true); //str_replace($value, $search, $replace);
|
||||
}
|
||||
}
|
||||
|
||||
if ($isJson && $in_deep === false) // convert back to JSON, if this was JSON. Different than serialize which does WP automatically.
|
||||
{
|
||||
Log::addDebug('Value was found to be JSON, encoding');
|
||||
// wp-slash -> WP does stripslashes_deep which destroys JSON
|
||||
$content = json_encode($content, JSON_UNESCAPED_SLASHES);
|
||||
Log::addDebug('Content returning', array($content));
|
||||
}
|
||||
elseif($in_deep === false && (is_array($content) || is_object($content)))
|
||||
$content = maybe_serialize($content);
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function change_key($arr, $set) {
|
||||
if (is_array($arr) && is_array($set)) {
|
||||
$newArr = array();
|
||||
foreach ($arr as $k => $v) {
|
||||
$key = array_key_exists( $k, $set) ? $set[$k] : $k;
|
||||
$newArr[$key] = is_array($v) ? $this->change_key($v, $set) : $v;
|
||||
}
|
||||
return $newArr;
|
||||
}
|
||||
return $arr;
|
||||
}
|
||||
|
||||
private function getRelativeURLS()
|
||||
{
|
||||
$dataArray = array(
|
||||
'source' => array('url' => $this->source_url, 'metadata' => $this->getFilesFromMetadata($this->source_metadata) ),
|
||||
'target' => array('url' => $this->target_url, 'metadata' => $this->getFilesFromMetadata($this->target_metadata) ),
|
||||
);
|
||||
|
||||
// Log::addDebug('Source Metadata', $this->source_metadata);
|
||||
// Log::addDebug('Target Metadata', $this->target_metadata);
|
||||
|
||||
$result = array();
|
||||
|
||||
foreach($dataArray as $index => $item)
|
||||
{
|
||||
$result[$index] = array();
|
||||
$metadata = $item['metadata'];
|
||||
|
||||
$baseurl = parse_url($item['url'], PHP_URL_PATH);
|
||||
$result[$index]['base'] = $baseurl; // this is the relpath of the mainfile.
|
||||
$baseurl = trailingslashit(str_replace( wp_basename($item['url']), '', $baseurl)); // get the relpath of main file.
|
||||
|
||||
foreach($metadata as $name => $filename)
|
||||
{
|
||||
$result[$index][$name] = $baseurl . wp_basename($filename); // filename can have a path like 19/08 etc.
|
||||
}
|
||||
|
||||
}
|
||||
// Log::addDebug('Relative URLS', $result);
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
private function getFilesFromMetadata($meta)
|
||||
{
|
||||
$fileArray = array();
|
||||
if (isset($meta['file']))
|
||||
$fileArray['file'] = $meta['file'];
|
||||
|
||||
if (isset($meta['sizes']))
|
||||
{
|
||||
foreach($meta['sizes'] as $name => $data)
|
||||
{
|
||||
if (isset($data['file']))
|
||||
{
|
||||
$fileArray[$name] = $data['file'];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $fileArray;
|
||||
}
|
||||
|
||||
/** FindNearestsize
|
||||
* This works on the assumption that when the exact image size name is not available, find the nearest width with the smallest possible difference to impact the site the least.
|
||||
*/
|
||||
private function findNearestSize($sizeName)
|
||||
{
|
||||
|
||||
if (! isset($this->source_metadata['sizes'][$sizeName]) || ! isset($this->target_metadata['width'])) // This can happen with non-image files like PDF.
|
||||
{
|
||||
// Check if metadata-less item is a svg file. Just the main file to replace all thumbnails since SVG's don't need thumbnails.
|
||||
if (strpos($this->target_url, '.svg') !== false)
|
||||
{
|
||||
$svg_file = wp_basename($this->target_url);
|
||||
return $svg_file; // this is the relpath of the mainfile.
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
$old_width = $this->source_metadata['sizes'][$sizeName]['width']; // the width from size not in new image
|
||||
$new_width = $this->target_metadata['width']; // default check - the width of the main image
|
||||
|
||||
$diff = abs($old_width - $new_width);
|
||||
// $closest_file = str_replace($this->relPath, '', $this->newMeta['file']);
|
||||
$closest_file = wp_basename($this->target_metadata['file']); // mainfile as default
|
||||
|
||||
foreach($this->target_metadata['sizes'] as $sizeName => $data)
|
||||
{
|
||||
$thisdiff = abs($old_width - $data['width']);
|
||||
|
||||
if ( $thisdiff < $diff )
|
||||
{
|
||||
$closest_file = $data['file'];
|
||||
if(is_array($closest_file)) { $closest_file = $closest_file[0];} // HelpScout case 709692915
|
||||
if(!empty($closest_file)) {
|
||||
$diff = $thisdiff;
|
||||
$found_metasize = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(empty($closest_file)) return false;
|
||||
|
||||
return $closest_file;
|
||||
}
|
||||
|
||||
/* Check if given content is JSON format. */
|
||||
private function isJSON($content)
|
||||
{
|
||||
if (is_array($content) || is_object($content) || is_null($content))
|
||||
return false; // can never be.
|
||||
|
||||
$json = json_decode($content);
|
||||
return $json && $json != $content;
|
||||
}
|
||||
|
||||
private function checkIncomplete($var)
|
||||
{
|
||||
return ($var instanceof \__PHP_Incomplete_Class);
|
||||
}
|
||||
|
||||
|
||||
} // class
|
Reference in New Issue
Block a user