| @@ -90,8 +90,10 @@ | ||
| 90 | 90 | if ( is_admin() ) { |
| 91 | 91 | $this->initAdmin(); |
| 92 | 92 | } |
| 93 | 93 | |
| 94 | + | |
| 95 | + | |
| 94 | 96 | // Initialize background services |
| 95 | 97 | \EasyInvoice\Services\QuoteExpirationService::init(); |
| 96 | 98 | } |
| 97 | 99 | |
| @@ -103,9 +105,18 @@ | ||
| 103 | 105 | * @return void |
| 104 | 106 | */ |
| 105 | 107 | private function defineConstants(): void { |
| 106 | 108 | if ( ! defined( 'EASY_INVOICE_VERSION' ) ) { |
| 107 | - define( 'EASY_INVOICE_VERSION', '1.0.0' ); | |
| 109 | + $fallback = '2.1.18'; | |
| 110 | + if ( defined( 'EASY_INVOICE_PLUGIN_FILE' ) && function_exists( 'get_file_data' ) ) { | |
| 111 | + $headers = get_file_data( | |
| 112 | + EASY_INVOICE_PLUGIN_FILE, | |
| 113 | + array( 'version' => 'Version' ), | |
| 114 | + 'plugin' | |
| 115 | + ); | |
| 116 | + $fallback = ! empty( $headers['version'] ) ? $headers['version'] : $fallback; | |
| 117 | + } | |
| 118 | + define( 'EASY_INVOICE_VERSION', $fallback ); | |
| 108 | 119 | } |
| 109 | 120 | |
| 110 | 121 | if ( ! defined( 'EASY_INVOICE_FILE' ) ) { |
| 111 | 122 | define( 'EASY_INVOICE_FILE', dirname( dirname( __FILE__ ) ) . '/easy-invoice.php' ); |
| @@ -153,10 +164,14 @@ | ||
| 153 | 164 | |
| 154 | 165 | // Add admin action to flush rewrite rules |
| 155 | 166 | add_action('admin_post_flush_easy_invoice_rewrite_rules', [$this, 'handleFlushRewriteRules']); |
| 156 | 167 | |
| 157 | - // Add admin action to fix quote slugs | |
| 158 | - add_action('admin_post_fix_easy_invoice_quote_slugs', [$this, 'handleFixQuoteSlugs']); | |
| 168 | + // NOTE: 'admin_post_fix_easy_invoice_quote_slugs' was registered here against | |
| 169 | + // [$this, 'handleFixQuoteSlugs'] — a method that does not exist on this class | |
| 170 | + // (or anywhere else in the plugin). Hitting that endpoint was an immediate | |
| 171 | + // fatal. Nothing in the UI ever linked to it, so the registration is removed | |
| 172 | + // rather than the method being written. Re-add both together if the | |
| 173 | + // quote-slug repair tool is ever actually needed. | |
| 159 | 174 | |
| 160 | 175 | // Add admin action to manually register post types |
| 161 | 176 | add_action('admin_post_register_easy_invoice_post_types', [$this, 'handleRegisterPostTypes']); |
| 162 | 177 | |
| @@ -210,12 +225,16 @@ | ||
| 210 | 225 | private function initPaymentGateways() { |
| 211 | 226 | if ( ! $this->gateway_manager ) { |
| 212 | 227 | $this->gateway_manager = new PaymentGatewayManager(); |
| 213 | 228 | |
| 214 | - // Register payment gateways. | |
| 215 | - $gateways = [ | |
| 216 | - new Gateways\PayPalGateway(), | |
| 217 | - ]; | |
| 229 | + // Register payment gateways: PayPal, then one gateway per offline | |
| 230 | + // method (bank transfer, cheque, cash and any the merchant added). | |
| 231 | + // The single "Manual Payment" gateway of earlier versions is gone; | |
| 232 | + // setDefaultPaymentMethods() carries its enabled state over. | |
| 233 | + $gateways = [ new Gateways\PayPalGateway() ]; | |
| 234 | + foreach ( Services\OfflinePayments::methods() as $method ) { | |
| 235 | + $gateways[] = new Gateways\OfflineGateway( $method ); | |
| 236 | + } | |
| 218 | 237 | |
| 219 | 238 | foreach ( $gateways as $gateway ) { |
| 220 | 239 | $this->gateway_manager->registerGateway( $gateway ); |
| 221 | 240 | } |
| @@ -235,15 +254,27 @@ | ||
| 235 | 254 | * @access private |
| 236 | 255 | * @return void |
| 237 | 256 | */ |
| 238 | 257 | private function setDefaultPaymentMethods() { |
| 239 | - $payment_methods = get_option( 'easy_invoice_payment_methods', [] ); | |
| 258 | + $payment_methods = get_option( 'easy_invoice_payment_methods', null ); | |
| 259 | + if ( null === $payment_methods ) { | |
| 260 | + // First run: an invoice with no way to pay is a dead end, and bank | |
| 261 | + // transfer needs no account with anyone. The merchant can switch it | |
| 262 | + // off under Settings → Payment Methods. | |
| 263 | + update_option( 'easy_invoice_payment_methods', [ 'bank_transfer' ] ); | |
| 264 | + return; | |
| 265 | + } | |
| 266 | + $payment_methods = is_array( $payment_methods ) ? $payment_methods : []; | |
| 240 | 267 | |
| 241 | - $defaults_updated = false; | |
| 242 | - | |
| 243 | - // Bank Transfer, Cheque, and Cash payment defaults moved to Pro plugin | |
| 244 | - | |
| 245 | - if ( $defaults_updated ) { | |
| 268 | + // Sites that had "Manual Payment" switched on keep an offline option: | |
| 269 | + // its three sub-choices are gateways of their own now. | |
| 270 | + if ( in_array( 'manual', $payment_methods, true ) ) { | |
| 271 | + $payment_methods = array_values( array_diff( $payment_methods, [ 'manual' ] ) ); | |
| 272 | + foreach ( [ 'bank_transfer', 'cheque', 'cash' ] as $offline ) { | |
| 273 | + if ( ! in_array( $offline, $payment_methods, true ) ) { | |
| 274 | + $payment_methods[] = $offline; | |
| 275 | + } | |
| 276 | + } | |
| 246 | 277 | update_option( 'easy_invoice_payment_methods', array_unique( $payment_methods ) ); |
| 247 | 278 | } |
| 248 | 279 | } |
| 249 | 280 | |
| @@ -293,8 +324,15 @@ | ||
| 293 | 324 | 'query_var' => true, |
| 294 | 325 | 'rewrite' => [ 'slug' => 'invoice' ], |
| 295 | 326 | 'capability_type' => 'post', |
| 296 | 327 | 'has_archive' => false, |
| 328 | + // Keep invoices out of site search, search feeds and any archive-style | |
| 329 | + // query. They are only ever reachable as an authorised single document | |
| 330 | + // (see TemplateLoader::enforceDocumentAccess). Without this, `?s=INV` | |
| 331 | + // and `?feed=rss2&post_type=easy_invoice` listed invoice titles and | |
| 332 | + // permalinks to anonymous visitors — enough to enumerate every invoice | |
| 333 | + // on the site even though the documents themselves are gated. | |
| 334 | + 'exclude_from_search' => true, | |
| 297 | 335 | 'hierarchical' => false, |
| 298 | 336 | 'menu_position' => null, |
| 299 | 337 | 'supports' => [ 'title', 'editor', 'custom-fields' ] |
| 300 | 338 | ]); |
| @@ -327,8 +365,10 @@ | ||
| 327 | 365 | 'query_var' => true, |
| 328 | 366 | 'rewrite' => [ 'slug' => 'easy-invoice-quote', 'with_front' => false ], |
| 329 | 367 | 'capability_type' => 'post', |
| 330 | 368 | 'has_archive' => false, |
| 369 | + // Same reasoning as the invoice post type above. | |
| 370 | + 'exclude_from_search' => true, | |
| 331 | 371 | 'hierarchical' => false, |
| 332 | 372 | 'menu_position' => null, |
| 333 | 373 | 'supports' => [ 'title', 'editor', 'custom-fields' ] |
| 334 | 374 | ]); |
| @@ -355,10 +395,13 @@ | ||
| 355 | 395 | ], |
| 356 | 396 | 'description' => __( 'Payments for Easy Invoice plugin.', 'easy-invoice' ), |
| 357 | 397 | 'public' => false, |
| 358 | 398 | 'publicly_queryable' => false, |
| 359 | - 'show_ui' => true, | |
| 360 | - 'show_in_menu' => 'edit.php?post_type=easy_invoice', | |
| 399 | + // Payments are managed on the plugin's own Payments screen; the | |
| 400 | + // stock post editor knows none of their fields and its "Add New" | |
| 401 | + // left nameless auto-drafts behind. | |
| 402 | + 'show_ui' => false, | |
| 403 | + 'show_in_menu' => false, | |
| 361 | 404 | 'query_var' => true, |
| 362 | 405 | 'rewrite' => [ 'slug' => 'payment' ], |
| 363 | 406 | 'capability_type' => 'post', |
| 364 | 407 | 'has_archive' => false, |
| @@ -367,14 +410,52 @@ | ||
| 367 | 410 | 'supports' => [ 'title', 'author', 'custom-fields' ], |
| 368 | 411 | 'show_in_rest' => false, |
| 369 | 412 | ] ); |
| 370 | 413 | |
| 414 | + // Credit notes. Not publicly queryable and with no rewrite: unlike an | |
| 415 | + // invoice, a credit note is never handed to a customer through a | |
| 416 | + // permalink — it reaches them as a PDF attached to the correction being | |
| 417 | + // explained, so there is no front-end URL to protect in the first place. | |
| 418 | + register_post_type( \EasyInvoice\Constants\PostTypes::EASY_INVOICE_CREDIT_NOTE_POST_TYPE, [ | |
| 419 | + 'labels' => [ | |
| 420 | + 'name' => _x( 'Credit Notes', 'post type general name', 'easy-invoice' ), | |
| 421 | + 'singular_name' => _x( 'Credit Note', 'post type singular name', 'easy-invoice' ), | |
| 422 | + 'menu_name' => _x( 'Credit Notes', 'admin menu', 'easy-invoice' ), | |
| 423 | + 'all_items' => __( 'All Credit Notes', 'easy-invoice' ), | |
| 424 | + 'edit_item' => __( 'Edit Credit Note', 'easy-invoice' ), | |
| 425 | + 'view_item' => __( 'View Credit Note', 'easy-invoice' ), | |
| 426 | + 'search_items' => __( 'Search Credit Notes', 'easy-invoice' ), | |
| 427 | + 'not_found' => __( 'No credit notes found.', 'easy-invoice' ), | |
| 428 | + 'not_found_in_trash' => __( 'No credit notes found in Trash.', 'easy-invoice' ), | |
| 429 | + ], | |
| 430 | + 'description' => __( 'Credit notes issued against invoices.', 'easy-invoice' ), | |
| 431 | + 'public' => false, | |
| 432 | + 'publicly_queryable' => false, | |
| 433 | + 'exclude_from_search'=> true, | |
| 434 | + 'show_ui' => false, | |
| 435 | + 'show_in_menu' => false, | |
| 436 | + 'query_var' => false, | |
| 437 | + 'rewrite' => false, | |
| 438 | + 'capability_type' => 'post', | |
| 439 | + 'has_archive' => false, | |
| 440 | + 'hierarchical' => false, | |
| 441 | + 'supports' => [ 'title', 'author', 'custom-fields' ], | |
| 442 | + 'show_in_rest' => false, | |
| 443 | + ] ); | |
| 371 | 444 | |
| 372 | - // Force flush rewrite rules after post type registration | |
| 445 | + | |
| 446 | + // Flush rewrite rules after post type registration. | |
| 447 | + // | |
| 448 | + // This is throttled to once every 5 minutes (see flushRewriteRules below). | |
| 449 | + // An unconditional `flush_rewrite_rules(true)` used to follow this call, | |
| 450 | + // which meant every single request — this method runs on `init` priority 0 — | |
| 451 | + // regenerated the whole rule set and wrote the `rewrite_rules` option. That | |
| 452 | + // is one of the most expensive things a plugin can do per request, and it | |
| 453 | + // made the throttle above pointless. | |
| 454 | + // | |
| 455 | + // Activation still flushes explicitly (see easy_invoice_activate), so new | |
| 456 | + // installs and permalink changes are covered without the per-request cost. | |
| 373 | 457 | $this->flushRewriteRules(); |
| 374 | - | |
| 375 | - // Force an immediate rewrite rules flush | |
| 376 | - flush_rewrite_rules(true); | |
| 377 | 458 | } |
| 378 | 459 | |
| 379 | 460 | /** |
| 380 | 461 | * Flush rewrite rules to ensure custom post type URLs work |
| @@ -588,8 +669,9 @@ | ||
| 588 | 669 | 'public' => true, |
| 589 | 670 | 'exclude_from_search' => false, |
| 590 | 671 | 'show_in_admin_all_list' => true, |
| 591 | 672 | 'show_in_admin_status_list' => true, |
| 673 | + /* translators: %s: number of items. */ | |
| 592 | 674 | 'label_count' => _n_noop( |
| 593 | 675 | 'Pending Bank Transfer <span class="count">(%s)</span>', |
| 594 | 676 | 'Pending Bank Transfer <span class="count">(%s)</span>', |
| 595 | 677 | 'easy-invoice' |
| @@ -600,8 +682,9 @@ | ||
| 600 | 682 | 'public' => true, |
| 601 | 683 | 'exclude_from_search' => false, |
| 602 | 684 | 'show_in_admin_all_list' => true, |
| 603 | 685 | 'show_in_admin_status_list' => true, |
| 686 | + /* translators: %s: number of items. */ | |
| 604 | 687 | 'label_count' => _n_noop( |
| 605 | 688 | 'Pending Cheque <span class="count">(%s)</span>', |
| 606 | 689 | 'Pending Cheque <span class="count">(%s)</span>', |
| 607 | 690 | 'easy-invoice' |
| @@ -638,9 +721,9 @@ | ||
| 638 | 721 | $redirect_url = admin_url('admin.php?page=easy-quote-all&rewrite_flushed=1'); |
| 639 | 722 | } |
| 640 | 723 | } |
| 641 | 724 | |
| 642 | - wp_redirect($redirect_url); | |
| 725 | + wp_safe_redirect($redirect_url); | |
| 643 | 726 | exit; |
| 644 | 727 | } |
| 645 | 728 | |
| 646 | 729 | /** |
| @@ -668,9 +751,9 @@ | ||
| 668 | 751 | $redirect_url = admin_url('admin.php?page=easy-quote-all&post_types_registered=1'); |
| 669 | 752 | } |
| 670 | 753 | } |
| 671 | 754 | |
| 672 | - wp_redirect($redirect_url); | |
| 755 | + wp_safe_redirect($redirect_url); | |
| 673 | 756 | exit; |
| 674 | 757 | } |
| 675 | 758 | |
| 676 | 759 | /** |
| @@ -682,38 +765,110 @@ | ||
| 682 | 765 | public function disableAdminNoticesOnEasyInvoicePages() { |
| 683 | 766 | // Get current page |
| 684 | 767 | $page = isset( $_GET['page'] ) ? sanitize_text_field( $_GET['page'] ) : ''; |
| 685 | 768 | |
| 686 | - // Check if we're on an Easy Invoice page | |
| 769 | + // Static list of core Easy Invoice page slugs. Addon-owned page | |
| 770 | + // slugs (Item Library, Template Builder, enterprise addon pages, | |
| 771 | + // export manager, etc.) are appended dynamically below from | |
| 772 | + // AddonRegistry so new addons automatically suppress WP notices | |
| 773 | + // on their pages without needing to edit this list. | |
| 687 | 774 | $easy_invoice_pages = [ |
| 688 | - 'easy-invoice', // Dashboard | |
| 689 | - 'easy-invoice-all', // All invoices | |
| 690 | - 'easy-invoice-builder', // Invoice builder | |
| 691 | - 'easy-invoice-preview', // Invoice preview | |
| 692 | - 'easy-quote-all', // All quotes | |
| 693 | - 'easy-invoice-quote-builder', // Quote builder | |
| 694 | - 'easy-quote-preview', // Quote preview | |
| 695 | - 'easy-invoice-payments', // All payments | |
| 696 | - 'easy-invoice-payment-new', // Add new payment | |
| 697 | - 'easy-invoice-clients', // All clients | |
| 698 | - 'easy-invoice-client-edit', // Edit client | |
| 699 | - 'easy-invoice-client-view', // View client | |
| 700 | - 'easy-invoice-reports', // Reports | |
| 701 | - 'easy-invoice-settings', // Main settings | |
| 702 | - 'easy-invoice-email-settings', // Email settings | |
| 775 | + 'easy-invoice', // Dashboard | |
| 776 | + 'easy-invoice-all', // All invoices | |
| 777 | + 'easy-invoice-builder', // Invoice builder | |
| 778 | + 'easy-invoice-preview', // Invoice preview | |
| 779 | + 'easy-quote-all', // All quotes | |
| 780 | + 'easy-invoice-quote-builder', // Quote builder | |
| 781 | + 'easy-quote-preview', // Quote preview | |
| 782 | + 'easy-invoice-payments', // All payments | |
| 783 | + 'easy-invoice-payment-new', // Add new payment | |
| 784 | + 'easy-invoice-clients', // All clients | |
| 785 | + 'easy-invoice-client-edit', // Edit client | |
| 786 | + 'easy-invoice-client-view', // View client | |
| 787 | + 'easy-invoice-reports', // Reports | |
| 788 | + 'easy-invoice-settings', // Main settings | |
| 789 | + 'easy-invoice-email-settings', // Email settings | |
| 703 | 790 | 'easy-invoice-email-settings-general', |
| 704 | 791 | 'easy-invoice-email-settings-invoice', |
| 705 | 792 | 'easy-invoice-email-settings-quote', |
| 706 | 793 | 'easy-invoice-email-settings-payment', |
| 707 | - 'easy-invoice-pro-settings', // Pro settings | |
| 708 | - 'easy-invoice-pro-translations', // Pro translations | |
| 709 | - 'easy-invoice-migration', // Migration page | |
| 710 | - 'easy-invoice-license', // License page | |
| 711 | - 'easy-invoice-free-vs-pro' | |
| 794 | + 'easy-invoice-pro-settings', // Pro settings | |
| 795 | + 'easy-invoice-pro-translations',// Pro translations | |
| 796 | + 'easy-invoice-pro-item-library',// Item Library (Pro) | |
| 797 | + 'easy-invoice-templates', // Template Builder listing (Pro) | |
| 798 | + 'easy-invoice-templates-new', // Template Builder "Create New" (Pro) | |
| 799 | + 'easy-invoice-template-builder',// Template Builder canvas (Pro) | |
| 800 | + 'easy-invoice-export', // Export Data (bulk_operations addon) | |
| 801 | + 'easy-invoice-addons', // Addons grid | |
| 802 | + 'easy-invoice-migration', // Migration page | |
| 803 | + 'easy-invoice-license', // License page | |
| 804 | + 'easy-invoice-free-vs-pro', | |
| 805 | + 'easy-invoice-join-community', | |
| 806 | + 'easy-invoice-import', | |
| 712 | 807 | ]; |
| 713 | 808 | |
| 714 | - // Check if current page is an Easy Invoice page | |
| 715 | - if ( in_array( $page, $easy_invoice_pages ) ) { | |
| 809 | + // Dynamically include every addon's `settings_url` page slug. | |
| 810 | + // This means enterprise addons (time-tracking, dunning, white-label, | |
| 811 | + // team-roles, webhooks) and any future addon automatically get | |
| 812 | + // notice-suppression without needing to update this array. | |
| 813 | + if ( class_exists( '\\EasyInvoice\\Addons\\AddonRegistry' ) ) { | |
| 814 | + foreach ( \EasyInvoice\Addons\AddonRegistry::all() as $ei_addon ) { | |
| 815 | + if ( empty( $ei_addon['settings_url'] ) ) { | |
| 816 | + continue; | |
| 817 | + } | |
| 818 | + $parts = wp_parse_url( $ei_addon['settings_url'] ); | |
| 819 | + if ( empty( $parts['query'] ) ) { | |
| 820 | + continue; | |
| 821 | + } | |
| 822 | + parse_str( $parts['query'], $qs ); | |
| 823 | + if ( ! empty( $qs['page'] ) ) { | |
| 824 | + $easy_invoice_pages[] = $qs['page']; | |
| 825 | + } | |
| 826 | + } | |
| 827 | + $easy_invoice_pages = array_values( array_unique( $easy_invoice_pages ) ); | |
| 828 | + } | |
| 829 | + | |
| 830 | + // Any Easy Invoice screen, including ones the list above cannot know about. | |
| 831 | + // | |
| 832 | + // The list is built from each addon's `settings_url`, which is only an addon's | |
| 833 | + // PRIMARY page. An addon that registers a second screen — Accounting Sync's | |
| 834 | + // "Sync Log" is the one that exists today — was therefore left out, and WordPress | |
| 835 | + // rendered its notices there. Those notices are emitted before this plugin's | |
| 836 | + // markup, so they land outside the app shell's content column and are clipped by | |
| 837 | + // the fixed sidebar: the page opened with truncated text across the top and the | |
| 838 | + // real heading pushed far down. It read as a broken page rather than a styled one. | |
| 839 | + // | |
| 840 | + // Matching on the slug prefix instead covers every Easy Invoice screen that | |
| 841 | + // exists now and any added later, and it lines up with how AdminAssets decides | |
| 842 | + // to load the admin CSS (`strpos($hook, 'easy-invoice') !== false`) — the two | |
| 843 | + // should always agree on what counts as one of our screens. | |
| 844 | + $is_easy_invoice_page = in_array( $page, $easy_invoice_pages, true ) | |
| 845 | + || strpos( $page, 'easy-invoice' ) === 0 | |
| 846 | + || strpos( $page, 'easy-quote' ) === 0; | |
| 847 | + | |
| 848 | + if ( $is_easy_invoice_page ) { | |
| 849 | + // Don't remove our review notice - keep it for free users | |
| 850 | + // Store our notice callback temporarily | |
| 851 | + $review_notice_callback = false; | |
| 852 | + $review_notice_priority = false; | |
| 853 | + | |
| 854 | + // Find our review notice hook | |
| 855 | + global $wp_filter; | |
| 856 | + if (isset($wp_filter['admin_notices']->callbacks)) { | |
| 857 | + foreach ($wp_filter['admin_notices']->callbacks as $priority => $callbacks) { | |
| 858 | + foreach ($callbacks as $callback) { | |
| 859 | + if (is_array($callback['function']) && | |
| 860 | + is_object($callback['function'][0]) && | |
| 861 | + $callback['function'][0] instanceof \EasyInvoice\Services\ReviewNoticeService && | |
| 862 | + $callback['function'][1] === 'displayAdminNotice') { | |
| 863 | + $review_notice_callback = $callback['function']; | |
| 864 | + $review_notice_priority = $priority; | |
| 865 | + break 2; | |
| 866 | + } | |
| 867 | + } | |
| 868 | + } | |
| 869 | + } | |
| 870 | + | |
| 716 | 871 | // Remove all admin notices by removing the action |
| 717 | 872 | remove_all_actions( 'admin_notices' ); |
| 718 | 873 | |
| 719 | 874 | // Also remove network and user admin notices |
| @@ -719,7 +874,12 @@ | ||
| 719 | 874 | // Also remove network and user admin notices |
| 720 | 875 | remove_all_actions( 'network_admin_notices' ); |
| 721 | 876 | remove_all_actions( 'user_admin_notices' ); |
| 722 | 877 | remove_all_actions( 'all_admin_notices' ); |
| 878 | + | |
| 879 | + // Re-add our review notice if it was found | |
| 880 | + if ($review_notice_callback !== false && $review_notice_priority !== false) { | |
| 881 | + add_action('admin_notices', $review_notice_callback, $review_notice_priority); | |
| 882 | + } | |
| 723 | 883 | } |
| 724 | 884 | } |
| 725 | 885 | } |