Changeset 3776328
- Timestamp:
- 10/31/2026 04:23:27 AM (14 hours ago)
- Location:
- mxp-dev-tools
- Files:
-
- 1 added
- 2 edited
-
tests/login-logo-regression.sh (added)
-
trunk/mxp-snippets.php (modified) (3 diffs)
-
trunk/readme.txt (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
mxp-dev-tools/trunk/mxp-snippets.php
r3766356 r3776328 322 322 private $honeypot; 323 323 private $honeypot_integrations = array(); 324 private $login_logo_url = ''; 324 325 325 326 public function __construct() { … … 488 489 add_filter('login_redirect', array($this, 'login_redirect'), 11, 3); 489 490 } 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 !== '') { 491 493 add_action('login_enqueue_scripts', array($this, 'login_css_enqueues')); 492 494 } … … 1682 1684 1683 1685 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 } 1686 1690 } 1687 1691 1688 1692 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 } 1738 1793 1739 1794 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}"; 1740 1830 } 1741 1831 -
mxp-dev-tools/trunk/readme.txt
r3766356 r3776328 52 52 * 檔案修改保護:預設啟用 `DISALLOW_FILE_MODS` / `DISALLOW_FILE_EDIT`,並保護指定管理員不被一般後台列表、編輯、刪除或降權流程影響。 53 53 * 登入與後台調整:登入頁 Logo、登入後導向、管理信箱同步、註冊開關、封鎖使用者登入、顯示內容/分類/使用者 ID、使用者註冊時間欄位、隱藏非管理員更新提示與 Customize 選單。 54 * 登入頁 Logo:在 WordPress 載入前定義 `MDT_LOGINPAGE_LOGO_URL`;只接受不含帳密的 HTTP/HTTPS URL。中文網域、路徑、查詢與 fragment 會正規化為 Punycode/percent-encoding,既有 `%XX` 編碼不會重複處理。 54 55 * 媒體與上傳政策:可停用縮圖產生、停用大圖縮放、上傳圖片大小限制、自動填入圖片標題/alt/content,並對特權管理員與一般使用者套用不同的檔案大小與 MIME 規則。 55 56 * Bot/垃圾防護:WordPress 公開登入、註冊、忘記/重設密碼、多站註冊、留言與文章密碼表單使用 action-bound honeypot 與短效簽章 token;WooCommerce 與 Contact Form 7 則各自提供預設關閉的獨立整合開關。 … … 176 177 Most 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. 177 178 179 = How do I change the WordPress login Logo? = 180 181 Define `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 178 183 = Will observed HTTP_HOST domains be shown on the domain expiry dashboard? = 179 184 … … 225 230 226 231 = 3.6.12 = 227 Snippets honeypot 完整保護 WordPress 公開內建表單,新增具抗快取 token 的 Contact Form 7 與 WooCommerce 獨立選用整合; 表單 adapter 已透過註冊契約、registry 與共用留言路由解耦,並完成 WordPress 5.5/PHP 7.4 相容驗證。232 Snippets honeypot 完整保護 WordPress 公開內建表單,新增具抗快取 token 的 Contact Form 7 與 WooCommerce 獨立選用整合;登入頁 Logo URL 同步支援中文網域與路徑並限制為安全的 HTTP/HTTPS 輸出;全版完成 WordPress 5.5/PHP 7.4 相容驗證。 228 233 229 234 = 3.6.11 = … … 269 274 270 275 * 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 外部圖片探測。 271 277 * Snippets honeypot 重構為可註冊 action/context 的核心與表單 adapter:受保護的 WordPress 公開內建表單現在強制要求有效短效簽章 token 與空白動態誘餌欄位,補齊忘記/重設密碼、多站註冊及文章密碼流程;快取頁面會從 `no-store` 端點刷新 token,並保留 WooCommerce Classic 帳號表單保護。 272 278 * Honeypot integrations 改由統一 contract 與 registry 註冊;共用留言 router 依優先序隔離 WordPress 留言與 WooCommerce 商品評論,即使個別 integration 關閉也不會被通用規則誤接管,後續新增表單 adapter 不必修改既有 adapter。
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)