| @@ -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 | |
| @@ -213,8 +228,9 @@ | ||
| 213 | 228 | |
| 214 | 229 | // Register payment gateways. |
| 215 | 230 | $gateways = [ |
| 216 | 231 | new Gateways\PayPalGateway(), |
| 232 | + new Gateways\ManualGateway(), | |
| 217 | 233 | ]; |
| 218 | 234 | |
| 219 | 235 | foreach ( $gateways as $gateway ) { |
| 220 | 236 | $this->gateway_manager->registerGateway( $gateway ); |
| @@ -293,8 +309,15 @@ | ||
| 293 | 309 | 'query_var' => true, |
| 294 | 310 | 'rewrite' => [ 'slug' => 'invoice' ], |
| 295 | 311 | 'capability_type' => 'post', |
| 296 | 312 | 'has_archive' => false, |
| 313 | + // Keep invoices out of site search, search feeds and any archive-style | |
| 314 | + // query. They are only ever reachable as an authorised single document | |
| 315 | + // (see TemplateLoader::enforceDocumentAccess). Without this, `?s=INV` | |
| 316 | + // and `?feed=rss2&post_type=easy_invoice` listed invoice titles and | |
| 317 | + // permalinks to anonymous visitors — enough to enumerate every invoice | |
| 318 | + // on the site even though the documents themselves are gated. | |
| 319 | + 'exclude_from_search' => true, | |
| 297 | 320 | 'hierarchical' => false, |
| 298 | 321 | 'menu_position' => null, |
| 299 | 322 | 'supports' => [ 'title', 'editor', 'custom-fields' ] |
| 300 | 323 | ]); |
| @@ -327,8 +350,10 @@ | ||
| 327 | 350 | 'query_var' => true, |
| 328 | 351 | 'rewrite' => [ 'slug' => 'easy-invoice-quote', 'with_front' => false ], |
| 329 | 352 | 'capability_type' => 'post', |
| 330 | 353 | 'has_archive' => false, |
| 354 | + // Same reasoning as the invoice post type above. | |
| 355 | + 'exclude_from_search' => true, | |
| 331 | 356 | 'hierarchical' => false, |
| 332 | 357 | 'menu_position' => null, |
| 333 | 358 | 'supports' => [ 'title', 'editor', 'custom-fields' ] |
| 334 | 359 | ]); |
| @@ -355,10 +380,13 @@ | ||
| 355 | 380 | ], |
| 356 | 381 | 'description' => __( 'Payments for Easy Invoice plugin.', 'easy-invoice' ), |
| 357 | 382 | 'public' => false, |
| 358 | 383 | 'publicly_queryable' => false, |
| 359 | - 'show_ui' => true, | |
| 360 | - 'show_in_menu' => 'edit.php?post_type=easy_invoice', | |
| 384 | + // Payments are managed on the plugin's own Payments screen; the | |
| 385 | + // stock post editor knows none of their fields and its "Add New" | |
| 386 | + // left nameless auto-drafts behind. | |
| 387 | + 'show_ui' => false, | |
| 388 | + 'show_in_menu' => false, | |
| 361 | 389 | 'query_var' => true, |
| 362 | 390 | 'rewrite' => [ 'slug' => 'payment' ], |
| 363 | 391 | 'capability_type' => 'post', |
| 364 | 392 | 'has_archive' => false, |
| @@ -367,14 +395,52 @@ | ||
| 367 | 395 | 'supports' => [ 'title', 'author', 'custom-fields' ], |
| 368 | 396 | 'show_in_rest' => false, |
| 369 | 397 | ] ); |
| 370 | 398 | |
| 399 | + // Credit notes. Not publicly queryable and with no rewrite: unlike an | |
| 400 | + // invoice, a credit note is never handed to a customer through a | |
| 401 | + // permalink — it reaches them as a PDF attached to the correction being | |
| 402 | + // explained, so there is no front-end URL to protect in the first place. | |
| 403 | + register_post_type( \EasyInvoice\Constants\PostTypes::EASY_INVOICE_CREDIT_NOTE_POST_TYPE, [ | |
| 404 | + 'labels' => [ | |
| 405 | + 'name' => _x( 'Credit Notes', 'post type general name', 'easy-invoice' ), | |
| 406 | + 'singular_name' => _x( 'Credit Note', 'post type singular name', 'easy-invoice' ), | |
| 407 | + 'menu_name' => _x( 'Credit Notes', 'admin menu', 'easy-invoice' ), | |
| 408 | + 'all_items' => __( 'All Credit Notes', 'easy-invoice' ), | |
| 409 | + 'edit_item' => __( 'Edit Credit Note', 'easy-invoice' ), | |
| 410 | + 'view_item' => __( 'View Credit Note', 'easy-invoice' ), | |
| 411 | + 'search_items' => __( 'Search Credit Notes', 'easy-invoice' ), | |
| 412 | + 'not_found' => __( 'No credit notes found.', 'easy-invoice' ), | |
| 413 | + 'not_found_in_trash' => __( 'No credit notes found in Trash.', 'easy-invoice' ), | |
| 414 | + ], | |
| 415 | + 'description' => __( 'Credit notes issued against invoices.', 'easy-invoice' ), | |
| 416 | + 'public' => false, | |
| 417 | + 'publicly_queryable' => false, | |
| 418 | + 'exclude_from_search'=> true, | |
| 419 | + 'show_ui' => false, | |
| 420 | + 'show_in_menu' => false, | |
| 421 | + 'query_var' => false, | |
| 422 | + 'rewrite' => false, | |
| 423 | + 'capability_type' => 'post', | |
| 424 | + 'has_archive' => false, | |
| 425 | + 'hierarchical' => false, | |
| 426 | + 'supports' => [ 'title', 'author', 'custom-fields' ], | |
| 427 | + 'show_in_rest' => false, | |
| 428 | + ] ); | |
| 371 | 429 | |
| 372 | - // Force flush rewrite rules after post type registration | |
| 430 | + | |
| 431 | + // Flush rewrite rules after post type registration. | |
| 432 | + // | |
| 433 | + // This is throttled to once every 5 minutes (see flushRewriteRules below). | |
| 434 | + // An unconditional `flush_rewrite_rules(true)` used to follow this call, | |
| 435 | + // which meant every single request — this method runs on `init` priority 0 — | |
| 436 | + // regenerated the whole rule set and wrote the `rewrite_rules` option. That | |
| 437 | + // is one of the most expensive things a plugin can do per request, and it | |
| 438 | + // made the throttle above pointless. | |
| 439 | + // | |
| 440 | + // Activation still flushes explicitly (see easy_invoice_activate), so new | |
| 441 | + // installs and permalink changes are covered without the per-request cost. | |
| 373 | 442 | $this->flushRewriteRules(); |
| 374 | - | |
| 375 | - // Force an immediate rewrite rules flush | |
| 376 | - flush_rewrite_rules(true); | |
| 377 | 443 | } |
| 378 | 444 | |
| 379 | 445 | /** |
| 380 | 446 | * Flush rewrite rules to ensure custom post type URLs work |
| @@ -588,8 +654,9 @@ | ||
| 588 | 654 | 'public' => true, |
| 589 | 655 | 'exclude_from_search' => false, |
| 590 | 656 | 'show_in_admin_all_list' => true, |
| 591 | 657 | 'show_in_admin_status_list' => true, |
| 658 | + /* translators: %s: number of items. */ | |
| 592 | 659 | 'label_count' => _n_noop( |
| 593 | 660 | 'Pending Bank Transfer <span class="count">(%s)</span>', |
| 594 | 661 | 'Pending Bank Transfer <span class="count">(%s)</span>', |
| 595 | 662 | 'easy-invoice' |
| @@ -600,8 +667,9 @@ | ||
| 600 | 667 | 'public' => true, |
| 601 | 668 | 'exclude_from_search' => false, |
| 602 | 669 | 'show_in_admin_all_list' => true, |
| 603 | 670 | 'show_in_admin_status_list' => true, |
| 671 | + /* translators: %s: number of items. */ | |
| 604 | 672 | 'label_count' => _n_noop( |
| 605 | 673 | 'Pending Cheque <span class="count">(%s)</span>', |
| 606 | 674 | 'Pending Cheque <span class="count">(%s)</span>', |
| 607 | 675 | 'easy-invoice' |
| @@ -638,9 +706,9 @@ | ||
| 638 | 706 | $redirect_url = admin_url('admin.php?page=easy-quote-all&rewrite_flushed=1'); |
| 639 | 707 | } |
| 640 | 708 | } |
| 641 | 709 | |
| 642 | - wp_redirect($redirect_url); | |
| 710 | + wp_safe_redirect($redirect_url); | |
| 643 | 711 | exit; |
| 644 | 712 | } |
| 645 | 713 | |
| 646 | 714 | /** |
| @@ -668,9 +736,9 @@ | ||
| 668 | 736 | $redirect_url = admin_url('admin.php?page=easy-quote-all&post_types_registered=1'); |
| 669 | 737 | } |
| 670 | 738 | } |
| 671 | 739 | |
| 672 | - wp_redirect($redirect_url); | |
| 740 | + wp_safe_redirect($redirect_url); | |
| 673 | 741 | exit; |
| 674 | 742 | } |
| 675 | 743 | |
| 676 | 744 | /** |
| @@ -682,38 +750,110 @@ | ||
| 682 | 750 | public function disableAdminNoticesOnEasyInvoicePages() { |
| 683 | 751 | // Get current page |
| 684 | 752 | $page = isset( $_GET['page'] ) ? sanitize_text_field( $_GET['page'] ) : ''; |
| 685 | 753 | |
| 686 | - // Check if we're on an Easy Invoice page | |
| 754 | + // Static list of core Easy Invoice page slugs. Addon-owned page | |
| 755 | + // slugs (Item Library, Template Builder, enterprise addon pages, | |
| 756 | + // export manager, etc.) are appended dynamically below from | |
| 757 | + // AddonRegistry so new addons automatically suppress WP notices | |
| 758 | + // on their pages without needing to edit this list. | |
| 687 | 759 | $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 | |
| 760 | + 'easy-invoice', // Dashboard | |
| 761 | + 'easy-invoice-all', // All invoices | |
| 762 | + 'easy-invoice-builder', // Invoice builder | |
| 763 | + 'easy-invoice-preview', // Invoice preview | |
| 764 | + 'easy-quote-all', // All quotes | |
| 765 | + 'easy-invoice-quote-builder', // Quote builder | |
| 766 | + 'easy-quote-preview', // Quote preview | |
| 767 | + 'easy-invoice-payments', // All payments | |
| 768 | + 'easy-invoice-payment-new', // Add new payment | |
| 769 | + 'easy-invoice-clients', // All clients | |
| 770 | + 'easy-invoice-client-edit', // Edit client | |
| 771 | + 'easy-invoice-client-view', // View client | |
| 772 | + 'easy-invoice-reports', // Reports | |
| 773 | + 'easy-invoice-settings', // Main settings | |
| 774 | + 'easy-invoice-email-settings', // Email settings | |
| 703 | 775 | 'easy-invoice-email-settings-general', |
| 704 | 776 | 'easy-invoice-email-settings-invoice', |
| 705 | 777 | 'easy-invoice-email-settings-quote', |
| 706 | 778 | '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' | |
| 779 | + 'easy-invoice-pro-settings', // Pro settings | |
| 780 | + 'easy-invoice-pro-translations',// Pro translations | |
| 781 | + 'easy-invoice-pro-item-library',// Item Library (Pro) | |
| 782 | + 'easy-invoice-templates', // Template Builder listing (Pro) | |
| 783 | + 'easy-invoice-templates-new', // Template Builder "Create New" (Pro) | |
| 784 | + 'easy-invoice-template-builder',// Template Builder canvas (Pro) | |
| 785 | + 'easy-invoice-export', // Export Data (bulk_operations addon) | |
| 786 | + 'easy-invoice-addons', // Addons grid | |
| 787 | + 'easy-invoice-migration', // Migration page | |
| 788 | + 'easy-invoice-license', // License page | |
| 789 | + 'easy-invoice-free-vs-pro', | |
| 790 | + 'easy-invoice-join-community', | |
| 791 | + 'easy-invoice-import', | |
| 712 | 792 | ]; |
| 713 | 793 | |
| 714 | - // Check if current page is an Easy Invoice page | |
| 715 | - if ( in_array( $page, $easy_invoice_pages ) ) { | |
| 794 | + // Dynamically include every addon's `settings_url` page slug. | |
| 795 | + // This means enterprise addons (time-tracking, dunning, white-label, | |
| 796 | + // team-roles, webhooks) and any future addon automatically get | |
| 797 | + // notice-suppression without needing to update this array. | |
| 798 | + if ( class_exists( '\\EasyInvoice\\Addons\\AddonRegistry' ) ) { | |
| 799 | + foreach ( \EasyInvoice\Addons\AddonRegistry::all() as $ei_addon ) { | |
| 800 | + if ( empty( $ei_addon['settings_url'] ) ) { | |
| 801 | + continue; | |
| 802 | + } | |
| 803 | + $parts = wp_parse_url( $ei_addon['settings_url'] ); | |
| 804 | + if ( empty( $parts['query'] ) ) { | |
| 805 | + continue; | |
| 806 | + } | |
| 807 | + parse_str( $parts['query'], $qs ); | |
| 808 | + if ( ! empty( $qs['page'] ) ) { | |
| 809 | + $easy_invoice_pages[] = $qs['page']; | |
| 810 | + } | |
| 811 | + } | |
| 812 | + $easy_invoice_pages = array_values( array_unique( $easy_invoice_pages ) ); | |
| 813 | + } | |
| 814 | + | |
| 815 | + // Any Easy Invoice screen, including ones the list above cannot know about. | |
| 816 | + // | |
| 817 | + // The list is built from each addon's `settings_url`, which is only an addon's | |
| 818 | + // PRIMARY page. An addon that registers a second screen — Accounting Sync's | |
| 819 | + // "Sync Log" is the one that exists today — was therefore left out, and WordPress | |
| 820 | + // rendered its notices there. Those notices are emitted before this plugin's | |
| 821 | + // markup, so they land outside the app shell's content column and are clipped by | |
| 822 | + // the fixed sidebar: the page opened with truncated text across the top and the | |
| 823 | + // real heading pushed far down. It read as a broken page rather than a styled one. | |
| 824 | + // | |
| 825 | + // Matching on the slug prefix instead covers every Easy Invoice screen that | |
| 826 | + // exists now and any added later, and it lines up with how AdminAssets decides | |
| 827 | + // to load the admin CSS (`strpos($hook, 'easy-invoice') !== false`) — the two | |
| 828 | + // should always agree on what counts as one of our screens. | |
| 829 | + $is_easy_invoice_page = in_array( $page, $easy_invoice_pages, true ) | |
| 830 | + || strpos( $page, 'easy-invoice' ) === 0 | |
| 831 | + || strpos( $page, 'easy-quote' ) === 0; | |
| 832 | + | |
| 833 | + if ( $is_easy_invoice_page ) { | |
| 834 | + // Don't remove our review notice - keep it for free users | |
| 835 | + // Store our notice callback temporarily | |
| 836 | + $review_notice_callback = false; | |
| 837 | + $review_notice_priority = false; | |
| 838 | + | |
| 839 | + // Find our review notice hook | |
| 840 | + global $wp_filter; | |
| 841 | + if (isset($wp_filter['admin_notices']->callbacks)) { | |
| 842 | + foreach ($wp_filter['admin_notices']->callbacks as $priority => $callbacks) { | |
| 843 | + foreach ($callbacks as $callback) { | |
| 844 | + if (is_array($callback['function']) && | |
| 845 | + is_object($callback['function'][0]) && | |
| 846 | + $callback['function'][0] instanceof \EasyInvoice\Services\ReviewNoticeService && | |
| 847 | + $callback['function'][1] === 'displayAdminNotice') { | |
| 848 | + $review_notice_callback = $callback['function']; | |
| 849 | + $review_notice_priority = $priority; | |
| 850 | + break 2; | |
| 851 | + } | |
| 852 | + } | |
| 853 | + } | |
| 854 | + } | |
| 855 | + | |
| 716 | 856 | // Remove all admin notices by removing the action |
| 717 | 857 | remove_all_actions( 'admin_notices' ); |
| 718 | 858 | |
| 719 | 859 | // Also remove network and user admin notices |
| @@ -719,7 +859,12 @@ | ||
| 719 | 859 | // Also remove network and user admin notices |
| 720 | 860 | remove_all_actions( 'network_admin_notices' ); |
| 721 | 861 | remove_all_actions( 'user_admin_notices' ); |
| 722 | 862 | remove_all_actions( 'all_admin_notices' ); |
| 863 | + | |
| 864 | + // Re-add our review notice if it was found | |
| 865 | + if ($review_notice_callback !== false && $review_notice_priority !== false) { | |
| 866 | + add_action('admin_notices', $review_notice_callback, $review_notice_priority); | |
| 867 | + } | |
| 723 | 868 | } |
| 724 | 869 | } |
| 725 | 870 | } |