Plugin Directory

Changeset 3776328


Ignore:
Timestamp:
10/31/2026 04:23:27 AM (14 hours ago)
Author:
mxp
Message:

Fix Unicode login logo URLs

Location:
mxp-dev-tools
Files:
1 added
2 edited

Legend:

Unmodified
Added
Removed
  • mxp-dev-tools/trunk/mxp-snippets.php

    r3766356 r3776328  
    322322        private $honeypot;
    323323        private $honeypot_integrations = array();
     324        private $login_logo_url = '';
    324325
    325326        public function __construct() {
    … …  
    488489                        add_filter('login_redirect', array($this, 'login_redirect'), 11, 3);
    489490                }
    490                 if (!empty(MDT_LOGINPAGE_LOGO_URL) && filter_var(MDT_LOGINPAGE_LOGO_URL, FILTER_VALIDATE_URL)) {
     491                $this->login_logo_url = $this->normalize_login_logo_url(MDT_LOGINPAGE_LOGO_URL);
     492                if ($this->login_logo_url !== '') {
    491493                        add_action('login_enqueue_scripts', array($this, 'login_css_enqueues'));
    492494                }
    … …  
    16821684
    16831685        public function login_css_enqueues() {
    1684                 echo '<style type="text/css">' . $this->admin_login_page_css(MDT_LOGINPAGE_LOGO_URL) . '</style>';
    1685 
     1686                $css = $this->build_admin_login_page_css($this->login_logo_url);
     1687                if ($css !== '') {
     1688                        echo '<style type="text/css">' . $css . '</style>';
     1689                }
    16861690        }
    16871691
    16881692        public function admin_login_page_css($image) {
    1689                 $headers = !empty($image) && ini_get('allow_url_fopen') ? @get_headers($image) : '';
    1690                 if (!empty($image) && $headers && (strpos($headers[0], '404') === false) && (strpos($headers[0],
    1691                         '403') === false) && ini_get('allow_url_fopen')) {
    1692                         $img_id = attachment_url_to_postid($image);
    1693                         if ($img_id) {
    1694                                 // First we check if the image has been uploaded on WordPress
    1695                                 $img_meta = wp_get_attachment_metadata($img_id);
    1696                                 if (isset($img_meta['width']) && isset($img_meta['height'])) {
    1697                                         $dimensions = array(
    1698                                                 $img_meta['width'],
    1699                                                 $img_meta['height'],
    1700                                         );
    1701                                 } else {
    1702                                         $dimensions = array(
    1703                                                 '',
    1704                                                 '',
    1705                                         );
    1706                                 }
    1707                         } else {
    1708                                 // If not (could be an external URL)
    1709                                 $dimensions = getimagesize($image);
    1710                         }
    1711                 } else {
    1712                         $dimensions = array(
    1713                                 '',
    1714                                 '',
    1715                         );
    1716                 }
    1717                 list($width, $height) = $dimensions; // Get the uploaded image's width and height
    1718                 if ($width != '' && $height != '' && $width < 321) {
    1719                         // If width is recognized, use it
    1720                         $w = $width . 'px auto';
    1721                         $h = 'height: ' . $height . 'px;';
    1722                 } elseif ($width > 320) {
    1723                         // but if it's more than 320 pixels, force it to 320px
    1724                         $r = ($width / $height); // calculate ratio
    1725                         $w = '320px auto';
    1726                         $h = 'height: ' . (320 / $r) . 'px;';
    1727                 } else {
    1728                         $w = 'auto 80px';
    1729                         $h = '';
    1730                 }
    1731                 $output = 'body.login div#login h1 a {
    1732                 background-image: url(' . $image . ');
    1733                 background-size: ' . $w . ';'
    1734                         . $h .
    1735                         'width: 100%;
    1736                 background-position: bottom;
    1737             }';
     1693                $image = $this->normalize_login_logo_url($image);
     1694                return $image === '' ? '' : $this->build_admin_login_page_css($image);
     1695        }
     1696
     1697        private function normalize_login_logo_url($url) {
     1698                if (!is_string($url)) {
     1699                        return '';
     1700                }
     1701
     1702                $url = trim(wp_check_invalid_utf8($url));
     1703                if ($url === '' || preg_match('/[\x00-\x1F\x7F]/', $url)) {
     1704                        return '';
     1705                }
     1706
     1707                $parts = wp_parse_url($url);
     1708                if (!is_array($parts) || empty($parts['scheme']) || empty($parts['host']) || isset($parts['user']) || isset($parts['pass'])) {
     1709                        return '';
     1710                }
     1711
     1712                $scheme = strtolower($parts['scheme']);
     1713                if (!in_array($scheme, array('http', 'https'), true)) {
     1714                        return '';
     1715                }
     1716
     1717                $host = $this->normalize_login_logo_host($parts['host']);
     1718                if ($host === '') {
     1719                        return '';
     1720                }
     1721
     1722                $port = '';
     1723                if (isset($parts['port'])) {
     1724                        $port_number = (int) $parts['port'];
     1725                        if ($port_number < 1 || $port_number > 65535) {
     1726                                return '';
     1727                        }
     1728                        $port = ':' . $port_number;
     1729                }
     1730
     1731                $path = isset($parts['path']) ? $this->encode_login_logo_uri_component($parts['path'], false) : '';
     1732                $query = isset($parts['query']) ? '?' . $this->encode_login_logo_uri_component($parts['query'], true) : '';
     1733                $fragment = isset($parts['fragment']) ? '#' . $this->encode_login_logo_uri_component($parts['fragment'], true) : '';
     1734                $normalized = $scheme . '://' . $host . $port . $path . $query . $fragment;
     1735                $normalized = esc_url_raw($normalized, array('http', 'https'));
     1736
     1737                return filter_var($normalized, FILTER_VALIDATE_URL) !== false ? $normalized : '';
     1738        }
     1739
     1740        private function normalize_login_logo_host($host) {
     1741                $host = trim((string) $host);
     1742                if ($host === '') {
     1743                        return '';
     1744                }
     1745
     1746                $has_brackets = $host[0] === '[' && substr($host, -1) === ']';
     1747                $unbracketed_host = $has_brackets ? substr($host, 1, -1) : $host;
     1748                if (filter_var($unbracketed_host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
     1749                        return '[' . strtolower($unbracketed_host) . ']';
     1750                }
     1751                if ($has_brackets || strpos($host, '[') !== false || strpos($host, ']') !== false) {
     1752                        return '';
     1753                }
     1754                if (filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
     1755                        return $host;
     1756                }
     1757
     1758                $normalizer = new \MxpDevTools\DomainExpiry\DomainNormalizer();
     1759                $host = $normalizer->to_ascii($host);
     1760                if ($host === '') {
     1761                        return '';
     1762                }
     1763                if ($host !== 'localhost' && !$normalizer->is_valid_ascii_domain($host)) {
     1764                        return '';
     1765                }
     1766
     1767                return $host;
     1768        }
     1769
     1770        private function encode_login_logo_uri_component($value, $allow_question_mark) {
     1771                $allowed = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~!$&\'()*+,;=:@/';
     1772                if ($allow_question_mark) {
     1773                        $allowed .= '?';
     1774                }
     1775
     1776                $hex = '0123456789ABCDEF';
     1777                $output = '';
     1778                $length = strlen($value);
     1779                for ($i = 0; $i < $length; $i++) {
     1780                        $character = $value[$i];
     1781                        if ($character === '%' && $i + 2 < $length && ctype_xdigit($value[$i + 1]) && ctype_xdigit($value[$i + 2])) {
     1782                                $output .= $character . $value[++$i] . $value[++$i];
     1783                                continue;
     1784                        }
     1785                        if (ord($character) < 128 && strpos($allowed, $character) !== false) {
     1786                                $output .= $character;
     1787                                continue;
     1788                        }
     1789
     1790                        $byte = ord($character);
     1791                        $output .= '%' . $hex[$byte >> 4] . $hex[$byte & 15];
     1792                }
    17381793
    17391794                return $output;
     1795        }
     1796
     1797        private function build_admin_login_page_css($image) {
     1798                if ($image === '') {
     1799                        return '';
     1800                }
     1801
     1802                $width = 0;
     1803                $height = 0;
     1804                $attachment_url = preg_replace('/[?#].*$/', '', $image);
     1805                $img_id = attachment_url_to_postid($attachment_url);
     1806                if ($img_id) {
     1807                        $img_meta = wp_get_attachment_metadata($img_id);
     1808                        if (is_array($img_meta) && isset($img_meta['width'], $img_meta['height'])) {
     1809                                $width = absint($img_meta['width']);
     1810                                $height = absint($img_meta['height']);
     1811                        }
     1812                }
     1813
     1814                $background_size = 'auto 80px';
     1815                $height_rule = '';
     1816                if ($width > 0 && $height > 0) {
     1817                        $display_width = min($width, 320);
     1818                        $display_height = max(1, (int) round($height * ($display_width / $width)));
     1819                        $background_size = $display_width . 'px auto';
     1820                        $height_rule = "\n\theight: " . $display_height . 'px;';
     1821                }
     1822
     1823                return 'body.login div#login h1 a {' .
     1824                        "\n\tbackground-image: url(\"" . $image . '");' .
     1825                        "\n\tbackground-size: " . $background_size . ';' .
     1826                        $height_rule .
     1827                        "\n\twidth: 100%;" .
     1828                        "\n\tbackground-position: bottom;" .
     1829                        "\n}";
    17401830        }
    17411831
  • mxp-dev-tools/trunk/readme.txt

    r3766356 r3776328  
    5252* 檔案修改保護:預設啟用 `DISALLOW_FILE_MODS` / `DISALLOW_FILE_EDIT`,並保護指定管理員不被一般後台列表、編輯、刪除或降權流程影響。
    5353* 登入與後台調整:登入頁 Logo、登入後導向、管理信箱同步、註冊開關、封鎖使用者登入、顯示內容/分類/使用者 ID、使用者註冊時間欄位、隱藏非管理員更新提示與 Customize 選單。
     54* 登入頁 Logo:在 WordPress 載入前定義 `MDT_LOGINPAGE_LOGO_URL`;只接受不含帳密的 HTTP/HTTPS URL。中文網域、路徑、查詢與 fragment 會正規化為 Punycode/percent-encoding,既有 `%XX` 編碼不會重複處理。
    5455* 媒體與上傳政策:可停用縮圖產生、停用大圖縮放、上傳圖片大小限制、自動填入圖片標題/alt/content,並對特權管理員與一般使用者套用不同的檔案大小與 MIME 規則。
    5556* Bot/垃圾防護:WordPress 公開登入、註冊、忘記/重設密碼、多站註冊、留言與文章密碼表單使用 action-bound honeypot 與短效簽章 token;WooCommerce 與 Contact Form 7 則各自提供預設關閉的獨立整合開關。
    … …  
    176177Most Snippets behavior is controlled with `MDT_*` constants defined before WordPress loads, usually in `wp-config.php`. Some behavior also has filters documented in the source.
    177178
     179= How do I change the WordPress login Logo? =
     180
     181Define `MDT_LOGINPAGE_LOGO_URL` before WordPress loads, for example `define('MDT_LOGINPAGE_LOGO_URL', 'https://example.com/uploads/品牌標誌.png');`. Snippets accepts Unicode in the host, path, query, and fragment, then normalizes the URL before emitting login-page CSS. Only HTTP/HTTPS URLs without embedded credentials are accepted. WordPress media attachments use their stored dimensions; external images use the safe default Logo size and are fetched by the browser rather than by the WordPress server.
     182
    178183= Will observed HTTP_HOST domains be shown on the domain expiry dashboard? =
    179184
    … …  
    225230
    226231= 3.6.12 =
    227 Snippets honeypot 完整保護 WordPress 公開內建表單,新增具抗快取 token 的 Contact Form 7 與 WooCommerce 獨立選用整合;表單 adapter 已透過註冊契約、registry 與共用留言路由解耦,並完成 WordPress 5.5/PHP 7.4 相容驗證。
     232Snippets honeypot 完整保護 WordPress 公開內建表單,新增具抗快取 token 的 Contact Form 7 與 WooCommerce 獨立選用整合;登入頁 Logo URL 同步支援中文網域與路徑並限制為安全的 HTTP/HTTPS 輸出;全版完成 WordPress 5.5/PHP 7.4 相容驗證。
    228233
    229234= 3.6.11 =
    … …  
    269274
    270275* Snippets honeypot 驗證會拒絕陣列等非字串 token 與留言文章 ID,避免畸形 POST 在 PHP 8 觸發 `explode()` TypeError 與 HTTP 500。
     276* 登入頁 `MDT_LOGINPAGE_LOGO_URL` 支援中文網域、路徑、query 與 fragment,自動產生 Punycode/percent-encoding 且保留既有 `%XX`;限制 http/https、拒絕內嵌帳密、以帶引號的 CSS URL 輸出,並移除登入請求中的 server-side 外部圖片探測。
    271277* Snippets honeypot 重構為可註冊 action/context 的核心與表單 adapter:受保護的 WordPress 公開內建表單現在強制要求有效短效簽章 token 與空白動態誘餌欄位,補齊忘記/重設密碼、多站註冊及文章密碼流程;快取頁面會從 `no-store` 端點刷新 token,並保留 WooCommerce Classic 帳號表單保護。
    272278* Honeypot integrations 改由統一 contract 與 registry 註冊;共用留言 router 依優先序隔離 WordPress 留言與 WooCommerce 商品評論,即使個別 integration 關閉也不會被通用規則誤接管,後續新增表單 adapter 不必修改既有 adapter。
Note: See TracChangeset for help on using the changeset viewer.