PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.0
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.0
2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.2 All 57 releases
← All changes | includes/EasyInvoice.php +139 -35 2.2.02.4.0 View file →
@@ -164,10 +164,14 @@
164 164
165 165 // Add admin action to flush rewrite rules
166 166 add_action('admin_post_flush_easy_invoice_rewrite_rules', [$this, 'handleFlushRewriteRules']);
167 167
168 - // Add admin action to fix quote slugs
169 - 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.
170 174
171 175 // Add admin action to manually register post types
172 176 add_action('admin_post_register_easy_invoice_post_types', [$this, 'handleRegisterPostTypes']);
173 177
@@ -305,8 +309,15 @@
305 309 'query_var' => true,
306 310 'rewrite' => [ 'slug' => 'invoice' ],
307 311 'capability_type' => 'post',
308 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,
309 320 'hierarchical' => false,
310 321 'menu_position' => null,
311 322 'supports' => [ 'title', 'editor', 'custom-fields' ]
312 323 ]);
@@ -339,8 +350,10 @@
339 350 'query_var' => true,
340 351 'rewrite' => [ 'slug' => 'easy-invoice-quote', 'with_front' => false ],
341 352 'capability_type' => 'post',
342 353 'has_archive' => false,
354 + // Same reasoning as the invoice post type above.
355 + 'exclude_from_search' => true,
343 356 'hierarchical' => false,
344 357 'menu_position' => null,
345 358 'supports' => [ 'title', 'editor', 'custom-fields' ]
346 359 ]);
@@ -367,10 +380,13 @@
367 380 ],
368 381 'description' => __( 'Payments for Easy Invoice plugin.', 'easy-invoice' ),
369 382 'public' => false,
370 383 'publicly_queryable' => false,
371 - 'show_ui' => true,
372 - '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,
373 389 'query_var' => true,
374 390 'rewrite' => [ 'slug' => 'payment' ],
375 391 'capability_type' => 'post',
376 392 'has_archive' => false,
@@ -379,14 +395,52 @@
379 395 'supports' => [ 'title', 'author', 'custom-fields' ],
380 396 'show_in_rest' => false,
381 397 ] );
382 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 + ] );
383 429
384 - // 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.
385 442 $this->flushRewriteRules();
386 -
387 - // Force an immediate rewrite rules flush
388 - flush_rewrite_rules(true);
389 443 }
390 444
391 445 /**
392 446 * Flush rewrite rules to ensure custom post type URLs work
@@ -600,8 +654,9 @@
600 654 'public' => true,
601 655 'exclude_from_search' => false,
602 656 'show_in_admin_all_list' => true,
603 657 'show_in_admin_status_list' => true,
658 + /* translators: %s: number of items. */
604 659 'label_count' => _n_noop(
605 660 'Pending Bank Transfer <span class="count">(%s)</span>',
606 661 'Pending Bank Transfer <span class="count">(%s)</span>',
607 662 'easy-invoice'
@@ -612,8 +667,9 @@
612 667 'public' => true,
613 668 'exclude_from_search' => false,
614 669 'show_in_admin_all_list' => true,
615 670 'show_in_admin_status_list' => true,
671 + /* translators: %s: number of items. */
616 672 'label_count' => _n_noop(
617 673 'Pending Cheque <span class="count">(%s)</span>',
618 674 'Pending Cheque <span class="count">(%s)</span>',
619 675 'easy-invoice'
@@ -650,9 +706,9 @@
650 706 $redirect_url = admin_url('admin.php?page=easy-quote-all&rewrite_flushed=1');
651 707 }
652 708 }
653 709
654 - wp_redirect($redirect_url);
710 + wp_safe_redirect($redirect_url);
655 711 exit;
656 712 }
657 713
658 714 /**
@@ -680,9 +736,9 @@
680 736 $redirect_url = admin_url('admin.php?page=easy-quote-all&post_types_registered=1');
681 737 }
682 738 }
683 739
684 - wp_redirect($redirect_url);
740 + wp_safe_redirect($redirect_url);
685 741 exit;
686 742 }
687 743
688 744 /**
@@ -694,40 +750,88 @@
694 750 public function disableAdminNoticesOnEasyInvoicePages() {
695 751 // Get current page
696 752 $page = isset( $_GET['page'] ) ? sanitize_text_field( $_GET['page'] ) : '';
697 753
698 - // 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.
699 759 $easy_invoice_pages = [
700 - 'easy-invoice', // Dashboard
701 - 'easy-invoice-all', // All invoices
702 - 'easy-invoice-builder', // Invoice builder
703 - 'easy-invoice-preview', // Invoice preview
704 - 'easy-quote-all', // All quotes
705 - 'easy-invoice-quote-builder', // Quote builder
706 - 'easy-quote-preview', // Quote preview
707 - 'easy-invoice-payments', // All payments
708 - 'easy-invoice-payment-new', // Add new payment
709 - 'easy-invoice-clients', // All clients
710 - 'easy-invoice-client-edit', // Edit client
711 - 'easy-invoice-client-view', // View client
712 - 'easy-invoice-reports', // Reports
713 - 'easy-invoice-settings', // Main settings
714 - '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
715 775 'easy-invoice-email-settings-general',
716 776 'easy-invoice-email-settings-invoice',
717 777 'easy-invoice-email-settings-quote',
718 778 'easy-invoice-email-settings-payment',
719 - 'easy-invoice-pro-settings', // Pro settings
720 - 'easy-invoice-pro-translations', // Pro translations
721 - 'easy-invoice-pro-item-library', // Item Library (Pro)
722 - 'easy-invoice-templates', // Template Builder (Pro)
723 - 'easy-invoice-migration', // Migration page
724 - 'easy-invoice-license', // License page
725 - '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',
726 792 ];
727 793
728 - // Check if current page is an Easy Invoice page
729 - 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 ) {
730 834 // Don't remove our review notice - keep it for free users
731 835 // Store our notice callback temporarily
732 836 $review_notice_callback = false;
733 837 $review_notice_priority = false;