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 +179 -34 2.1.22.4.0 View file →
@@ -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 }