Plugin Directory

Changeset 3709161


Ignore:
Timestamp:
09/23/2026 11:12:53 AM (4 weeks ago)
Author:
georget777
Message:

1.3.48: an image whose web address has no file extension (Unsplash, for example) is copied into the Media Library at import, and the page's own styles point at the copy

Location:
aitoel-html-importer
Files:
6 edited
1 copied

Legend:

Unmodified
Added
Removed
  • aitoel-html-importer/tags/1.3.48/aitoel-html-importer.php

    r3703741 r3709161  
    33 * Plugin Name: AI to Elementor — HTML Importer for Elementor
    44 * Description: Convert any HTML page into a fully editable Elementor template with one click. Works with AI page generators (Lovable, v0, Stitch, Cursor, bolt.new) and hand-written HTML.
    5  * Version: 1.3.47
     5 * Version: 1.3.48
    66 * Author: AI to Elementor
    77 * Author URI: https://aitoelementor.com
    … …  
    2424defined('ABSPATH') || exit;
    2525
    26 define('HTEL_VERSION', '1.3.47');
     26define('HTEL_VERSION', '1.3.48');
    2727define('HTEL_PLUGIN_FILE', __FILE__);
    2828define('HTEL_PLUGIN_DIR', plugin_dir_path(__FILE__));
  • aitoel-html-importer/tags/1.3.48/includes/class-converter.php

    r3703741 r3709161  
    16491649     * @return array Content, rewritten where a download succeeded.
    16501650     */
     1651    /**
     1652     * The absolute URLs held in the image fields the engine writes: an Image widget's `image`, a
     1653     * container's `background_image` and `background_overlay_image`, and a video's poster
     1654     * `image_overlay`. Links and video URLs are not among them.
     1655     */
     1656    private static function image_field_urls($elements) {
     1657        $urls = array();
     1658        foreach ((array) $elements as $el) {
     1659            foreach (array('image', 'background_image', 'background_overlay_image', 'image_overlay') as $key) {
     1660                $url = isset($el['settings'][$key]['url']) ? $el['settings'][$key]['url'] : null;
     1661                if (is_string($url) && preg_match('#^https?://#i', $url)) {
     1662                    $urls[] = $url;
     1663                }
     1664            }
     1665            if (isset($el['elements'])) {
     1666                $urls = array_merge($urls, self::image_field_urls($el['elements']));
     1667            }
     1668        }
     1669        return $urls;
     1670    }
     1671
     1672    /**
     1673     * Each URL replaced where it stands whole in the text. It ends where a URL ends in CSS or
     1674     * markup — a quote, a bracket, a comma, white space, an escaped quote (`"`) — and not where
     1675     * a URL goes on: a path, a query, an `&`. Nor is it replaced inside another URL.
     1676     *
     1677     * @param string $text
     1678     * @param array  $urls from => to
     1679     * @return string
     1680     */
     1681    private static function replace_whole_urls($text, $urls) {
     1682        $url_char = 'A-Za-z0-9._\~:\/?#\[\]@!$*+=%\x80-\xFF-';
     1683        foreach ($urls as $from => $to) {
     1684            if (strpos($text, $from) === false) {
     1685                continue;
     1686            }
     1687            $text = preg_replace_callback(
     1688                '~(?<![' . $url_char . '])' . preg_quote($from, '~')
     1689                    . '(?![' . $url_char . ']|&(?!quot;|apos;|#0*39;|#x0*27;|lt;|gt;))~',
     1690                function () use ($to) {
     1691                    return $to;
     1692                },
     1693                $text
     1694            );
     1695        }
     1696        return $text;
     1697    }
     1698
    16511699    private static function import_remote_images($content, $post_id) {
    16521700        if (!apply_filters('htel_import_remote_images', true, $post_id)) {
    … …  
    16721720        // the escaped form that is actually present.
    16731721        $scan = str_replace('\/', '/', $json);
    1674         if (!preg_match_all('#https?://[^"\s\x5C]+?\.(?:jpe?g|png|gif|webp|avif|svg)(?:\?[^"\s\x5C]*)?#i', $scan, $m)) {
    1675             return $content;
     1722        preg_match_all('#https?://[^"\s\x5C]+?\.(?:jpe?g|png|gif|webp|avif|svg)(?:\?[^"\s\x5C]*)?#i', $scan, $m);
     1723        // An image field holds an image whatever its URL looks like (image CDNs such as Unsplash serve
     1724        // without an extension), so its URL is fetched too; a file found only that way is named from its bytes.
     1725        $by_extension = array();
     1726        foreach ($m[0] as $url) {
     1727            $by_extension[html_entity_decode($url, ENT_QUOTES)] = true;
    16761728        }
    16771729        $home_host = wp_parse_url(home_url(), PHP_URL_HOST);
    16781730        $urls = array();
    1679         foreach ($m[0] as $url) {
     1731        foreach (array_merge($m[0], self::image_field_urls($content)) as $url) {
    16801732            $url = html_entity_decode($url, ENT_QUOTES);
    16811733            if (isset($urls[$url])) {
    … …  
    16991751        $started = microtime(true);
    17001752        $map     = array();
     1753        $whole   = array();
    17011754        $failed  = 0;
    17021755        $count   = 0;
    … …  
    17301783            }
    17311784            $count++;
     1785            $by_bytes = !isset($by_extension[$url]);
    17321786            $tmp = download_url($url, $file_timeout);
    17331787            if (is_wp_error($tmp)) {
    … …  
    17381792            if ($name === '' || strpos($name, '.') === false) {
    17391793                $name = 'image-' . $count . '.jpg';
     1794            }
     1795            if ($by_bytes) {
     1796                // Refused here when the bytes are not an image, even for a user WordPress would let
     1797                // upload a file of any type (unfiltered_upload).
     1798                $ext = self::image_extension_from_signature((string) file_get_contents($tmp, false, null, 0, 64));
     1799                if ($ext === '') {
     1800                    wp_delete_file($tmp);
     1801                    $failed++;
     1802                    continue;
     1803                }
     1804                $base = wp_basename(wp_parse_url($url, PHP_URL_PATH));
     1805                $name = ($base !== '' ? sanitize_file_name($base) : 'image-' . $count) . '.' . $ext;
    17401806            }
    17411807            $file_array = array('name' => $name, 'tmp_name' => $tmp);
    … …  
    17551821                continue;
    17561822            }
     1823            if ($by_bytes) {
     1824                // Repointed wherever it stands whole — the field, and the page's own CSS carried
     1825                // beside it (`url(…)` in the styles block) — but never where it begins or sits
     1826                // inside a longer URL: the same photo at another size, a gallery page.
     1827                $whole[$url] = $wp_url;
     1828                continue;
     1829            }
    17571830            // Rewrite the JSON-encoded form of the URL, which is what actually appears in
    17581831            // $json — a raw string replace would miss any escaped solidus.
    1759             $from = trim(wp_json_encode($url), '"');
    1760             $to   = trim(wp_json_encode($wp_url), '"');
    1761             $map[$from] = $to;
     1832            $map[trim(wp_json_encode($url), '"')] = trim(wp_json_encode($wp_url), '"');
    17621833        }
    17631834
    17641835        self::$last_remote_images = array(
    17651836            'found'    => count($urls),
    1766             'imported' => count($map),
     1837            'imported' => count($map) + count($whole),
    17671838            'failed'   => $failed,
    17681839        );
    … …  
    17831854        self::report_drops(array('remotefail' => $failed, 'remotecap' => $capped));
    17841855
    1785         if (empty($map)) {
     1856        if (empty($map) && empty($whole)) {
    17861857            return $content;
    17871858        }
    … …  
    18041875            return $content;
    18051876        }
     1877        array_walk_recursive($decoded, function (&$value) use ($whole) {
     1878            if (is_string($value)) {
     1879                $value = self::replace_whole_urls($value, $whole);
     1880            }
     1881        });
    18061882        return $decoded;
    18071883    }
  • aitoel-html-importer/tags/1.3.48/readme.txt

    r3703741 r3709161  
    66Tested up to: 7.1
    77Requires PHP: 7.4
    8 Stable tag: 1.3.47
     8Stable tag: 1.3.48
    99License: GPLv2 or later
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    202202
    203203== Changelog ==
     204
     205= 1.3.48 =
     206* Fixed: an image whose web address has no file extension (Unsplash's, for example) stayed linked
     207  to the site it came from after import. When such an image arrives as an Image widget or a
     208  background, it is now copied into your Media Library like any other image, and your page's own
     209  styles are pointed at the copy too. A file that turns out not to be an image is left where it was.
     210* Applies to pages you convert from now on. Templates you have already imported are not changed.
    204211
    205212= 1.3.47 =
    … …  
    452459== Upgrade Notice ==
    453460
     461= 1.3.48 =
     462Images whose web address has no file extension, such as Unsplash's, are now copied into your Media Library when they arrive as Image widgets or backgrounds.
     463
    454464= 1.3.47 =
    455465Less CSS in converted headings' Title fields: where the page looks exactly the same, their styling moves into the page's stylesheet.
  • aitoel-html-importer/trunk/aitoel-html-importer.php

    r3703741 r3709161  
    33 * Plugin Name: AI to Elementor — HTML Importer for Elementor
    44 * Description: Convert any HTML page into a fully editable Elementor template with one click. Works with AI page generators (Lovable, v0, Stitch, Cursor, bolt.new) and hand-written HTML.
    5  * Version: 1.3.47
     5 * Version: 1.3.48
    66 * Author: AI to Elementor
    77 * Author URI: https://aitoelementor.com
    … …  
    2424defined('ABSPATH') || exit;
    2525
    26 define('HTEL_VERSION', '1.3.47');
     26define('HTEL_VERSION', '1.3.48');
    2727define('HTEL_PLUGIN_FILE', __FILE__);
    2828define('HTEL_PLUGIN_DIR', plugin_dir_path(__FILE__));
  • aitoel-html-importer/trunk/includes/class-converter.php

    r3703741 r3709161  
    16491649     * @return array Content, rewritten where a download succeeded.
    16501650     */
     1651    /**
     1652     * The absolute URLs held in the image fields the engine writes: an Image widget's `image`, a
     1653     * container's `background_image` and `background_overlay_image`, and a video's poster
     1654     * `image_overlay`. Links and video URLs are not among them.
     1655     */
     1656    private static function image_field_urls($elements) {
     1657        $urls = array();
     1658        foreach ((array) $elements as $el) {
     1659            foreach (array('image', 'background_image', 'background_overlay_image', 'image_overlay') as $key) {
     1660                $url = isset($el['settings'][$key]['url']) ? $el['settings'][$key]['url'] : null;
     1661                if (is_string($url) && preg_match('#^https?://#i', $url)) {
     1662                    $urls[] = $url;
     1663                }
     1664            }
     1665            if (isset($el['elements'])) {
     1666                $urls = array_merge($urls, self::image_field_urls($el['elements']));
     1667            }
     1668        }
     1669        return $urls;
     1670    }
     1671
     1672    /**
     1673     * Each URL replaced where it stands whole in the text. It ends where a URL ends in CSS or
     1674     * markup — a quote, a bracket, a comma, white space, an escaped quote (`&quot;`) — and not where
     1675     * a URL goes on: a path, a query, an `&amp;`. Nor is it replaced inside another URL.
     1676     *
     1677     * @param string $text
     1678     * @param array  $urls from => to
     1679     * @return string
     1680     */
     1681    private static function replace_whole_urls($text, $urls) {
     1682        $url_char = 'A-Za-z0-9._\~:\/?#\[\]@!$*+=%\x80-\xFF-';
     1683        foreach ($urls as $from => $to) {
     1684            if (strpos($text, $from) === false) {
     1685                continue;
     1686            }
     1687            $text = preg_replace_callback(
     1688                '~(?<![' . $url_char . '])' . preg_quote($from, '~')
     1689                    . '(?![' . $url_char . ']|&(?!quot;|apos;|#0*39;|#x0*27;|lt;|gt;))~',
     1690                function () use ($to) {
     1691                    return $to;
     1692                },
     1693                $text
     1694            );
     1695        }
     1696        return $text;
     1697    }
     1698
    16511699    private static function import_remote_images($content, $post_id) {
    16521700        if (!apply_filters('htel_import_remote_images', true, $post_id)) {
    … …  
    16721720        // the escaped form that is actually present.
    16731721        $scan = str_replace('\/', '/', $json);
    1674         if (!preg_match_all('#https?://[^"\s\x5C]+?\.(?:jpe?g|png|gif|webp|avif|svg)(?:\?[^"\s\x5C]*)?#i', $scan, $m)) {
    1675             return $content;
     1722        preg_match_all('#https?://[^"\s\x5C]+?\.(?:jpe?g|png|gif|webp|avif|svg)(?:\?[^"\s\x5C]*)?#i', $scan, $m);
     1723        // An image field holds an image whatever its URL looks like (image CDNs such as Unsplash serve
     1724        // without an extension), so its URL is fetched too; a file found only that way is named from its bytes.
     1725        $by_extension = array();
     1726        foreach ($m[0] as $url) {
     1727            $by_extension[html_entity_decode($url, ENT_QUOTES)] = true;
    16761728        }
    16771729        $home_host = wp_parse_url(home_url(), PHP_URL_HOST);
    16781730        $urls = array();
    1679         foreach ($m[0] as $url) {
     1731        foreach (array_merge($m[0], self::image_field_urls($content)) as $url) {
    16801732            $url = html_entity_decode($url, ENT_QUOTES);
    16811733            if (isset($urls[$url])) {
    … …  
    16991751        $started = microtime(true);
    17001752        $map     = array();
     1753        $whole   = array();
    17011754        $failed  = 0;
    17021755        $count   = 0;
    … …  
    17301783            }
    17311784            $count++;
     1785            $by_bytes = !isset($by_extension[$url]);
    17321786            $tmp = download_url($url, $file_timeout);
    17331787            if (is_wp_error($tmp)) {
    … …  
    17381792            if ($name === '' || strpos($name, '.') === false) {
    17391793                $name = 'image-' . $count . '.jpg';
     1794            }
     1795            if ($by_bytes) {
     1796                // Refused here when the bytes are not an image, even for a user WordPress would let
     1797                // upload a file of any type (unfiltered_upload).
     1798                $ext = self::image_extension_from_signature((string) file_get_contents($tmp, false, null, 0, 64));
     1799                if ($ext === '') {
     1800                    wp_delete_file($tmp);
     1801                    $failed++;
     1802                    continue;
     1803                }
     1804                $base = wp_basename(wp_parse_url($url, PHP_URL_PATH));
     1805                $name = ($base !== '' ? sanitize_file_name($base) : 'image-' . $count) . '.' . $ext;
    17401806            }
    17411807            $file_array = array('name' => $name, 'tmp_name' => $tmp);
    … …  
    17551821                continue;
    17561822            }
     1823            if ($by_bytes) {
     1824                // Repointed wherever it stands whole — the field, and the page's own CSS carried
     1825                // beside it (`url(…)` in the styles block) — but never where it begins or sits
     1826                // inside a longer URL: the same photo at another size, a gallery page.
     1827                $whole[$url] = $wp_url;
     1828                continue;
     1829            }
    17571830            // Rewrite the JSON-encoded form of the URL, which is what actually appears in
    17581831            // $json — a raw string replace would miss any escaped solidus.
    1759             $from = trim(wp_json_encode($url), '"');
    1760             $to   = trim(wp_json_encode($wp_url), '"');
    1761             $map[$from] = $to;
     1832            $map[trim(wp_json_encode($url), '"')] = trim(wp_json_encode($wp_url), '"');
    17621833        }
    17631834
    17641835        self::$last_remote_images = array(
    17651836            'found'    => count($urls),
    1766             'imported' => count($map),
     1837            'imported' => count($map) + count($whole),
    17671838            'failed'   => $failed,
    17681839        );
    … …  
    17831854        self::report_drops(array('remotefail' => $failed, 'remotecap' => $capped));
    17841855
    1785         if (empty($map)) {
     1856        if (empty($map) && empty($whole)) {
    17861857            return $content;
    17871858        }
    … …  
    18041875            return $content;
    18051876        }
     1877        array_walk_recursive($decoded, function (&$value) use ($whole) {
     1878            if (is_string($value)) {
     1879                $value = self::replace_whole_urls($value, $whole);
     1880            }
     1881        });
    18061882        return $decoded;
    18071883    }
  • aitoel-html-importer/trunk/readme.txt

    r3703741 r3709161  
    66Tested up to: 7.1
    77Requires PHP: 7.4
    8 Stable tag: 1.3.47
     8Stable tag: 1.3.48
    99License: GPLv2 or later
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    202202
    203203== Changelog ==
     204
     205= 1.3.48 =
     206* Fixed: an image whose web address has no file extension (Unsplash's, for example) stayed linked
     207  to the site it came from after import. When such an image arrives as an Image widget or a
     208  background, it is now copied into your Media Library like any other image, and your page's own
     209  styles are pointed at the copy too. A file that turns out not to be an image is left where it was.
     210* Applies to pages you convert from now on. Templates you have already imported are not changed.
    204211
    205212= 1.3.47 =
    … …  
    452459== Upgrade Notice ==
    453460
     461= 1.3.48 =
     462Images whose web address has no file extension, such as Unsplash's, are now copied into your Media Library when they arrive as Image widgets or backgrounds.
     463
    454464= 1.3.47 =
    455465Less CSS in converted headings' Title fields: where the page looks exactly the same, their styling moves into the page's stylesheet.
Note: See TracChangeset for help on using the changeset viewer.