← All changes
|
admin/pages/license/src/Services/AddonsService.php
+84
-26
3.1.5
→
3.2.0
View file →
| @@ -29,8 +29,9 @@ | ||
| 29 | 29 | * only offers updates for licenses that are active/trial AND activated for this domain. |
| 30 | 30 | * Expired licenses are NOT offered updates (addon keeps working but no new versions). |
| 31 | 31 | */ |
| 32 | 32 | private $update_check_done = false; |
| 33 | + private $pruned = false; | |
| 33 | 34 | private $all_addons_transient_name; |
| 34 | 35 | private $signature_check_hook; |
| 35 | 36 | private $license_check_hook; |
| 36 | 37 | private $tampered_option; |
| @@ -1359,8 +1360,10 @@ | ||
| 1359 | 1360 | * Checks every installed plugin that matches a known addon slug from the proxy. |
| 1360 | 1361 | * Uses a grace period: show FATAL notice first, deactivate after TAMPER_GRACE_DAYS. |
| 1361 | 1362 | */ |
| 1362 | 1363 | public function verify_all_addon_signatures(): void { |
| 1364 | + $this->prune_missing_addons(); | |
| 1365 | + | |
| 1363 | 1366 | // Skip all checks on development/local/staging environments |
| 1364 | 1367 | if( LicenseModule::is_development_site() ) return; |
| 1365 | 1368 | |
| 1366 | 1369 | // Get the full list of known addon slugs from the proxy server |
| @@ -1518,12 +1521,14 @@ | ||
| 1518 | 1521 | * Records the first time admin sees each tamper notice. |
| 1519 | 1522 | */ |
| 1520 | 1523 | public function track_tamper_notice_view(): void { |
| 1521 | 1524 | if( ! current_user_can( 'administrator' ) ) return; |
| 1522 | - | |
| 1525 | + | |
| 1526 | + $this->prune_missing_addons(); | |
| 1527 | + | |
| 1523 | 1528 | $tampered = get_option( $this->tampered_option, [] ); |
| 1524 | 1529 | if( empty( $tampered ) ) return; |
| 1525 | - | |
| 1530 | + | |
| 1526 | 1531 | $seen = get_option( $this->tamper_dismissed_option, [] ); |
| 1527 | 1532 | $updated = false; |
| 1528 | 1533 | |
| 1529 | 1534 | foreach( $tampered as $slug => $info ) { |
| @@ -1562,19 +1567,70 @@ | ||
| 1562 | 1567 | set_transient( $flag, 1, 12 * HOUR_IN_SECONDS ); |
| 1563 | 1568 | } |
| 1564 | 1569 | |
| 1565 | 1570 | /** |
| 1566 | - * When a plugin is deleted, clear its tamper flag if it had one. | |
| 1571 | + * When a plugin is deleted, forget all stored notice/tamper/legacy data for it. | |
| 1567 | 1572 | */ |
| 1568 | 1573 | public function on_plugin_deleted( string $plugin_file, bool $deleted ): void { |
| 1569 | 1574 | if( ! $deleted ) return; |
| 1570 | - | |
| 1575 | + | |
| 1571 | 1576 | $slug = dirname( $plugin_file ); |
| 1572 | 1577 | if( $slug && $slug !== '.' ) { |
| 1573 | - $this->clear_tamper_flag( $slug ); | |
| 1574 | - $this->clear_legacy_cache( $slug ); | |
| 1578 | + $this->forget_addon( $slug ); | |
| 1575 | 1579 | } |
| 1576 | 1580 | } |
| 1581 | + | |
| 1582 | + /** | |
| 1583 | + * Check if an addon physically exists on disk as a real plugin. | |
| 1584 | + * An empty leftover folder (no plugin header file) counts as not present. | |
| 1585 | + */ | |
| 1586 | + private function is_addon_present( string $plugin_slug ): bool { | |
| 1587 | + if( empty( $plugin_slug ) || ! is_dir( WP_PLUGIN_DIR . '/' . $plugin_slug ) ) return false; | |
| 1588 | + | |
| 1589 | + return ! empty( $this->get_installed_plugin_file( $plugin_slug ) ); | |
| 1590 | + } | |
| 1591 | + | |
| 1592 | + /** | |
| 1593 | + * Remove all per-addon notice, tamper and legacy-cache data for a slug. | |
| 1594 | + * License records are intentionally kept — they are paid entitlements used by the store page. | |
| 1595 | + */ | |
| 1596 | + private function forget_addon( string $plugin_slug ): void { | |
| 1597 | + $expired = get_option( $this->expired_notice_option, [] ); | |
| 1598 | + if( isset( $expired[ $plugin_slug ] ) ) { | |
| 1599 | + unset( $expired[ $plugin_slug ] ); | |
| 1600 | + update_option( $this->expired_notice_option, $expired ); | |
| 1601 | + } | |
| 1602 | + | |
| 1603 | + $this->clear_tamper_flag( $plugin_slug ); | |
| 1604 | + $this->clear_legacy_cache( $plugin_slug ); | |
| 1605 | + | |
| 1606 | + foreach( [ 'tampered', 'expired', 'legacy' ] as $type ) { | |
| 1607 | + delete_transient( 'gvectors_' . $type . '_dismissed_' . $plugin_slug ); | |
| 1608 | + } | |
| 1609 | + } | |
| 1610 | + | |
| 1611 | + /** | |
| 1612 | + * Rewind stored per-addon data for addons that no longer physically exist | |
| 1613 | + * (e.g. deleted via FTP / file manager, bypassing the deleted_plugin hook). | |
| 1614 | + * Runs once per request per instance. | |
| 1615 | + */ | |
| 1616 | + public function prune_missing_addons(): void { | |
| 1617 | + if( $this->pruned ) return; | |
| 1618 | + $this->pruned = true; | |
| 1619 | + | |
| 1620 | + $slugs = []; | |
| 1621 | + foreach( [ $this->expired_notice_option, $this->tampered_option, $this->tamper_dismissed_option, $this->legacy_licenses_option, $this->legacy_notice_option ] as $option ) { | |
| 1622 | + $data = get_option( $option, [] ); | |
| 1623 | + if( is_array( $data ) ) $slugs = array_merge( $slugs, array_keys( $data ) ); | |
| 1624 | + } | |
| 1625 | + | |
| 1626 | + foreach( array_unique( $slugs ) as $slug ) { | |
| 1627 | + $slug = (string) $slug; | |
| 1628 | + if( ! $this->is_addon_present( $slug ) ) { | |
| 1629 | + $this->forget_addon( $slug ); | |
| 1630 | + } | |
| 1631 | + } | |
| 1632 | + } | |
| 1577 | 1633 | |
| 1578 | 1634 | /** |
| 1579 | 1635 | * Clear the legacy license cache for a specific addon. |
| 1580 | 1636 | */ |
| @@ -1600,17 +1656,22 @@ | ||
| 1600 | 1656 | * Marks expired licenses for admin notice display. |
| 1601 | 1657 | * Does NOT deactivate addons for expired licenses - they keep working. |
| 1602 | 1658 | */ |
| 1603 | 1659 | public function check_all_license_validity(): void { |
| 1660 | + $this->prune_missing_addons(); | |
| 1661 | + | |
| 1604 | 1662 | $licenses = $this->licenseService->get_all(); |
| 1605 | 1663 | if( empty( $licenses ) ) return; |
| 1606 | - | |
| 1664 | + | |
| 1607 | 1665 | $expired_notices = get_option( $this->expired_notice_option, [] ); |
| 1608 | - | |
| 1666 | + | |
| 1609 | 1667 | foreach( $licenses as $license ) { |
| 1610 | 1668 | $plugin_slug = $license['plugin_slug'] ?? ''; |
| 1611 | 1669 | if( empty( $plugin_slug ) ) continue; |
| 1612 | - if( ! $this->is_installed( $plugin_slug ) ) continue; | |
| 1670 | + if( ! $this->is_installed( $plugin_slug ) ) { | |
| 1671 | + unset( $expired_notices[ $plugin_slug ] ); | |
| 1672 | + continue; | |
| 1673 | + } | |
| 1613 | 1674 | |
| 1614 | 1675 | $status = $license['status'] ?? ''; |
| 1615 | 1676 | $expires_at = $license['expires_at'] ?? ''; |
| 1616 | 1677 | $is_expired = false; |
| @@ -1840,26 +1901,18 @@ | ||
| 1840 | 1901 | */ |
| 1841 | 1902 | public function tampered_addon_notice(): void { |
| 1842 | 1903 | if( ! $this->is_notice_page() ) return; |
| 1843 | 1904 | if( ! current_user_can( 'administrator' ) ) return; |
| 1905 | + | |
| 1906 | + $this->prune_missing_addons(); | |
| 1907 | + | |
| 1844 | 1908 | if( LicenseModule::is_development_site() ) return; |
| 1845 | - | |
| 1909 | + | |
| 1846 | 1910 | $tampered = get_option( $this->tampered_option, [] ); |
| 1847 | 1911 | if( empty( $tampered ) ) return; |
| 1848 | - | |
| 1849 | - $changed = false; | |
| 1912 | + | |
| 1850 | 1913 | foreach( $tampered as $slug => $info ) { |
| 1851 | - if( ! is_dir( WP_PLUGIN_DIR . '/' . $slug ) ) { | |
| 1852 | - unset( $tampered[ $slug ] ); | |
| 1853 | - $changed = true; | |
| 1854 | - } | |
| 1855 | - } | |
| 1856 | - if( $changed ) { | |
| 1857 | - update_option( $this->tampered_option, $tampered ); | |
| 1858 | - } | |
| 1859 | - if( empty( $tampered ) ) return; | |
| 1860 | - | |
| 1861 | - foreach( $tampered as $slug => $info ) { | |
| 1914 | + if( ! $this->is_addon_present( $slug ) ) continue; | |
| 1862 | 1915 | if( get_transient( 'gvectors_tampered_dismissed_' . $slug ) ) continue; |
| 1863 | 1916 | |
| 1864 | 1917 | $files = $info['files'] ?? []; |
| 1865 | 1918 | $reason = $info['reason'] ?? 'tampered'; |
| @@ -2018,12 +2071,15 @@ | ||
| 2018 | 2071 | public function expired_license_notice(): void { |
| 2019 | 2072 | if( ! $this->is_notice_page() ) return; |
| 2020 | 2073 | if( ! current_user_can( 'administrator' ) ) return; |
| 2021 | 2074 | |
| 2075 | + $this->prune_missing_addons(); | |
| 2076 | + | |
| 2022 | 2077 | $expired = get_option( $this->expired_notice_option, [] ); |
| 2023 | 2078 | if( empty( $expired ) ) return; |
| 2024 | - | |
| 2079 | + | |
| 2025 | 2080 | foreach( $expired as $slug => $info ) { |
| 2081 | + if( ! $this->is_addon_present( $slug ) ) continue; | |
| 2026 | 2082 | if( get_transient( 'gvectors_expired_dismissed_' . $slug ) ) continue; |
| 2027 | 2083 | |
| 2028 | 2084 | $product_name = $info['product_name'] ?? $slug; |
| 2029 | 2085 | $has_update = ! empty( $info['has_update'] ); |
| @@ -2074,11 +2130,13 @@ | ||
| 2074 | 2130 | public function legacy_license_notice(): void { |
| 2075 | 2131 | if( ! $this->is_notice_page() ) return; |
| 2076 | 2132 | if( ! current_user_can( 'administrator' ) ) return; |
| 2077 | 2133 | |
| 2134 | + $this->prune_missing_addons(); | |
| 2135 | + | |
| 2078 | 2136 | $notices = get_option( $this->legacy_notice_option, [] ); |
| 2079 | 2137 | if( empty( $notices ) ) return; |
| 2080 | - | |
| 2138 | + | |
| 2081 | 2139 | $addons_page_url = admin_url( $this->config->get_dashboard_addons_store_url() ); |
| 2082 | 2140 | |
| 2083 | 2141 | foreach( $notices as $slug => $info ) { |
| 2084 | 2142 | // Only show notices for expired legacy licenses |
| @@ -2084,9 +2142,9 @@ | ||
| 2084 | 2142 | // Only show notices for expired legacy licenses |
| 2085 | 2143 | if( empty( $info['status'] ) || $info['status'] !== 'expired' ) continue; |
| 2086 | 2144 | |
| 2087 | 2145 | // Verify the addon is still installed |
| 2088 | - if( ! is_dir( WP_PLUGIN_DIR . '/' . $slug ) ) continue; | |
| 2146 | + if( ! $this->is_addon_present( $slug ) ) continue; | |
| 2089 | 2147 | |
| 2090 | 2148 | if( get_transient( 'gvectors_legacy_dismissed_' . $slug ) ) continue; |
| 2091 | 2149 | |
| 2092 | 2150 | $plugin_name = $info['plugin_name'] ?? $slug; |