| @@ -105,9 +105,18 @@ | ||
| 105 | 105 | * @return void |
| 106 | 106 | */ |
| 107 | 107 | private function defineConstants(): void { |
| 108 | 108 | if ( ! defined( 'EASY_INVOICE_VERSION' ) ) { |
| 109 | - 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 ); | |
| 110 | 119 | } |
| 111 | 120 | |
| 112 | 121 | if ( ! defined( 'EASY_INVOICE_FILE' ) ) { |
| 113 | 122 | define( 'EASY_INVOICE_FILE', dirname( dirname( __FILE__ ) ) . '/easy-invoice.php' ); |
| @@ -155,10 +164,14 @@ | ||
| 155 | 164 | |
| 156 | 165 | // Add admin action to flush rewrite rules |
| 157 | 166 | add_action('admin_post_flush_easy_invoice_rewrite_rules', [$this, 'handleFlushRewriteRules']); |
| 158 | 167 | |
| 159 | - // Add admin action to fix quote slugs | |
| 160 | - 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. | |
| 161 | 174 | |
| 162 | 175 | // Add admin action to manually register post types |
| 163 | 176 | add_action('admin_post_register_easy_invoice_post_types', [$this, 'handleRegisterPostTypes']); |
| 164 | 177 | |
| @@ -212,12 +225,16 @@ | ||
| 212 | 225 | private function initPaymentGateways() { |
| 213 | 226 | if ( ! $this->gateway_manager ) { |
| 214 | 227 | $this->gateway_manager = new PaymentGatewayManager(); |
| 215 | 228 | |
| 216 | - // Register payment gateways. | |
| 217 | - $gateways = [ | |
| 218 | - new Gateways\PayPalGateway(), | |
| 219 | - ]; | |
| 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 | + } | |
| 220 | 237 | |
| 221 | 238 | foreach ( $gateways as $gateway ) { |
| 222 | 239 | $this->gateway_manager->registerGateway( $gateway ); |
| 223 | 240 | } |
| @@ -237,15 +254,27 @@ | ||
| 237 | 254 | * @access private |
| 238 | 255 | * @return void |
| 239 | 256 | */ |
| 240 | 257 | private function setDefaultPaymentMethods() { |
| 241 | - $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 : []; | |
| 242 | 267 | |
| 243 | - $defaults_updated = false; | |
| 244 | - | |
| 245 | - // Bank Transfer, Cheque, and Cash payment defaults moved to Pro plugin | |
| 246 | - | |
| 247 | - 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 | + } | |
| 248 | 277 | update_option( 'easy_invoice_payment_methods', array_unique( $payment_methods ) ); |
| 249 | 278 | } |
| 250 | 279 | } |
| 251 | 280 | |
| @@ -295,8 +324,15 @@ | ||
| 295 | 324 | 'query_var' => true, |
| 296 | 325 | 'rewrite' => [ 'slug' => 'invoice' ], |
| 297 | 326 | 'capability_type' => 'post', |
| 298 | 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, | |
| 299 | 335 | 'hierarchical' => false, |
| 300 | 336 | 'menu_position' => null, |
| 301 | 337 | 'supports' => [ 'title', 'editor', 'custom-fields' ] |
| 302 | 338 | ]); |
| @@ -329,8 +365,10 @@ | ||
| 329 | 365 | 'query_var' => true, |
| 330 | 366 | 'rewrite' => [ 'slug' => 'easy-invoice-quote', 'with_front' => false ], |
| 331 | 367 | 'capability_type' => 'post', |
| 332 | 368 | 'has_archive' => false, |
| 369 | + // Same reasoning as the invoice post type above. | |
| 370 | + 'exclude_from_search' => true, | |
| 333 | 371 | 'hierarchical' => false, |
| 334 | 372 | 'menu_position' => null, |
| 335 | 373 | 'supports' => [ 'title', 'editor', 'custom-fields' ] |
| 336 | 374 | ]); |
| @@ -357,10 +395,13 @@ | ||
| 357 | 395 | ], |
| 358 | 396 | 'description' => __( 'Payments for Easy Invoice plugin.', 'easy-invoice' ), |
| 359 | 397 | 'public' => false, |
| 360 | 398 | 'publicly_queryable' => false, |
| 361 | - 'show_ui' => true, | |
| 362 | - '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, | |
| 363 | 404 | 'query_var' => true, |
| 364 | 405 | 'rewrite' => [ 'slug' => 'payment' ], |
| 365 | 406 | 'capability_type' => 'post', |
| 366 | 407 | 'has_archive' => false, |
| @@ -369,14 +410,52 @@ | ||
| 369 | 410 | 'supports' => [ 'title', 'author', 'custom-fields' ], |
| 370 | 411 | 'show_in_rest' => false, |
| 371 | 412 | ] ); |
| 372 | 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 | + ] ); | |
| 373 | 444 | |
| 374 | - // 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. | |
| 375 | 457 | $this->flushRewriteRules(); |
| 376 | - | |
| 377 | - // Force an immediate rewrite rules flush | |
| 378 | - flush_rewrite_rules(true); | |
| 379 | 458 | } |
| 380 | 459 | |
| 381 | 460 | /** |
| 382 | 461 | * Flush rewrite rules to ensure custom post type URLs work |
| @@ -590,8 +669,9 @@ | ||
| 590 | 669 | 'public' => true, |
| 591 | 670 | 'exclude_from_search' => false, |
| 592 | 671 | 'show_in_admin_all_list' => true, |
| 593 | 672 | 'show_in_admin_status_list' => true, |
| 673 | + /* translators: %s: number of items. */ | |
| 594 | 674 | 'label_count' => _n_noop( |
| 595 | 675 | 'Pending Bank Transfer <span class="count">(%s)</span>', |
| 596 | 676 | 'Pending Bank Transfer <span class="count">(%s)</span>', |
| 597 | 677 | 'easy-invoice' |
| @@ -602,8 +682,9 @@ | ||
| 602 | 682 | 'public' => true, |
| 603 | 683 | 'exclude_from_search' => false, |
| 604 | 684 | 'show_in_admin_all_list' => true, |
| 605 | 685 | 'show_in_admin_status_list' => true, |
| 686 | + /* translators: %s: number of items. */ | |
| 606 | 687 | 'label_count' => _n_noop( |
| 607 | 688 | 'Pending Cheque <span class="count">(%s)</span>', |
| 608 | 689 | 'Pending Cheque <span class="count">(%s)</span>', |
| 609 | 690 | 'easy-invoice' |
| @@ -640,9 +721,9 @@ | ||
| 640 | 721 | $redirect_url = admin_url('admin.php?page=easy-quote-all&rewrite_flushed=1'); |
| 641 | 722 | } |
| 642 | 723 | } |
| 643 | 724 | |
| 644 | - wp_redirect($redirect_url); | |
| 725 | + wp_safe_redirect($redirect_url); | |
| 645 | 726 | exit; |
| 646 | 727 | } |
| 647 | 728 | |
| 648 | 729 | /** |
| @@ -670,9 +751,9 @@ | ||
| 670 | 751 | $redirect_url = admin_url('admin.php?page=easy-quote-all&post_types_registered=1'); |
| 671 | 752 | } |
| 672 | 753 | } |
| 673 | 754 | |
| 674 | - wp_redirect($redirect_url); | |
| 755 | + wp_safe_redirect($redirect_url); | |
| 675 | 756 | exit; |
| 676 | 757 | } |
| 677 | 758 | |
| 678 | 759 | /** |
| @@ -684,40 +765,88 @@ | ||
| 684 | 765 | public function disableAdminNoticesOnEasyInvoicePages() { |
| 685 | 766 | // Get current page |
| 686 | 767 | $page = isset( $_GET['page'] ) ? sanitize_text_field( $_GET['page'] ) : ''; |
| 687 | 768 | |
| 688 | - // 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. | |
| 689 | 774 | $easy_invoice_pages = [ |
| 690 | - 'easy-invoice', // Dashboard | |
| 691 | - 'easy-invoice-all', // All invoices | |
| 692 | - 'easy-invoice-builder', // Invoice builder | |
| 693 | - 'easy-invoice-preview', // Invoice preview | |
| 694 | - 'easy-quote-all', // All quotes | |
| 695 | - 'easy-invoice-quote-builder', // Quote builder | |
| 696 | - 'easy-quote-preview', // Quote preview | |
| 697 | - 'easy-invoice-payments', // All payments | |
| 698 | - 'easy-invoice-payment-new', // Add new payment | |
| 699 | - 'easy-invoice-clients', // All clients | |
| 700 | - 'easy-invoice-client-edit', // Edit client | |
| 701 | - 'easy-invoice-client-view', // View client | |
| 702 | - 'easy-invoice-reports', // Reports | |
| 703 | - 'easy-invoice-settings', // Main settings | |
| 704 | - '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 | |
| 705 | 790 | 'easy-invoice-email-settings-general', |
| 706 | 791 | 'easy-invoice-email-settings-invoice', |
| 707 | 792 | 'easy-invoice-email-settings-quote', |
| 708 | 793 | 'easy-invoice-email-settings-payment', |
| 709 | - 'easy-invoice-pro-settings', // Pro settings | |
| 710 | - 'easy-invoice-pro-translations', // Pro translations | |
| 711 | - 'easy-invoice-pro-item-library', // Item Library (Pro) | |
| 712 | - 'easy-invoice-templates', // Template Builder (Pro) | |
| 713 | - 'easy-invoice-migration', // Migration page | |
| 714 | - 'easy-invoice-license', // License page | |
| 715 | - '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', | |
| 716 | 807 | ]; |
| 717 | 808 | |
| 718 | - // Check if current page is an Easy Invoice page | |
| 719 | - 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 ) { | |
| 720 | 849 | // Don't remove our review notice - keep it for free users |
| 721 | 850 | // Store our notice callback temporarily |
| 722 | 851 | $review_notice_callback = false; |
| 723 | 852 | $review_notice_priority = false; |