| @@ -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 | |
| @@ -221,13 +225,16 @@ | ||
| 221 | 225 | private function initPaymentGateways() { |
| 222 | 226 | if ( ! $this->gateway_manager ) { |
| 223 | 227 | $this->gateway_manager = new PaymentGatewayManager(); |
| 224 | 228 | |
| 225 | - // Register payment gateways. | |
| 226 | - $gateways = [ | |
| 227 | - new Gateways\PayPalGateway(), | |
| 228 | - new Gateways\ManualGateway(), | |
| 229 | - ]; | |
| 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 | + } | |
| 230 | 237 | |
| 231 | 238 | foreach ( $gateways as $gateway ) { |
| 232 | 239 | $this->gateway_manager->registerGateway( $gateway ); |
| 233 | 240 | } |
| @@ -247,15 +254,27 @@ | ||
| 247 | 254 | * @access private |
| 248 | 255 | * @return void |
| 249 | 256 | */ |
| 250 | 257 | private function setDefaultPaymentMethods() { |
| 251 | - $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 : []; | |
| 252 | 267 | |
| 253 | - $defaults_updated = false; | |
| 254 | - | |
| 255 | - // Bank Transfer, Cheque, and Cash payment defaults moved to Pro plugin | |
| 256 | - | |
| 257 | - 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 | + } | |
| 258 | 277 | update_option( 'easy_invoice_payment_methods', array_unique( $payment_methods ) ); |
| 259 | 278 | } |
| 260 | 279 | } |
| 261 | 280 | |
| @@ -305,8 +324,15 @@ | ||
| 305 | 324 | 'query_var' => true, |
| 306 | 325 | 'rewrite' => [ 'slug' => 'invoice' ], |
| 307 | 326 | 'capability_type' => 'post', |
| 308 | 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, | |
| 309 | 335 | 'hierarchical' => false, |
| 310 | 336 | 'menu_position' => null, |
| 311 | 337 | 'supports' => [ 'title', 'editor', 'custom-fields' ] |
| 312 | 338 | ]); |
| @@ -339,8 +365,10 @@ | ||
| 339 | 365 | 'query_var' => true, |
| 340 | 366 | 'rewrite' => [ 'slug' => 'easy-invoice-quote', 'with_front' => false ], |
| 341 | 367 | 'capability_type' => 'post', |
| 342 | 368 | 'has_archive' => false, |
| 369 | + // Same reasoning as the invoice post type above. | |
| 370 | + 'exclude_from_search' => true, | |
| 343 | 371 | 'hierarchical' => false, |
| 344 | 372 | 'menu_position' => null, |
| 345 | 373 | 'supports' => [ 'title', 'editor', 'custom-fields' ] |
| 346 | 374 | ]); |
| @@ -367,10 +395,13 @@ | ||
| 367 | 395 | ], |
| 368 | 396 | 'description' => __( 'Payments for Easy Invoice plugin.', 'easy-invoice' ), |
| 369 | 397 | 'public' => false, |
| 370 | 398 | 'publicly_queryable' => false, |
| 371 | - 'show_ui' => true, | |
| 372 | - '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, | |
| 373 | 404 | 'query_var' => true, |
| 374 | 405 | 'rewrite' => [ 'slug' => 'payment' ], |
| 375 | 406 | 'capability_type' => 'post', |
| 376 | 407 | 'has_archive' => false, |
| @@ -379,14 +410,52 @@ | ||
| 379 | 410 | 'supports' => [ 'title', 'author', 'custom-fields' ], |
| 380 | 411 | 'show_in_rest' => false, |
| 381 | 412 | ] ); |
| 382 | 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 | + ] ); | |
| 383 | 444 | |
| 384 | - // 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. | |
| 385 | 457 | $this->flushRewriteRules(); |
| 386 | - | |
| 387 | - // Force an immediate rewrite rules flush | |
| 388 | - flush_rewrite_rules(true); | |
| 389 | 458 | } |
| 390 | 459 | |
| 391 | 460 | /** |
| 392 | 461 | * Flush rewrite rules to ensure custom post type URLs work |
| @@ -600,8 +669,9 @@ | ||
| 600 | 669 | 'public' => true, |
| 601 | 670 | 'exclude_from_search' => false, |
| 602 | 671 | 'show_in_admin_all_list' => true, |
| 603 | 672 | 'show_in_admin_status_list' => true, |
| 673 | + /* translators: %s: number of items. */ | |
| 604 | 674 | 'label_count' => _n_noop( |
| 605 | 675 | 'Pending Bank Transfer <span class="count">(%s)</span>', |
| 606 | 676 | 'Pending Bank Transfer <span class="count">(%s)</span>', |
| 607 | 677 | 'easy-invoice' |
| @@ -612,8 +682,9 @@ | ||
| 612 | 682 | 'public' => true, |
| 613 | 683 | 'exclude_from_search' => false, |
| 614 | 684 | 'show_in_admin_all_list' => true, |
| 615 | 685 | 'show_in_admin_status_list' => true, |
| 686 | + /* translators: %s: number of items. */ | |
| 616 | 687 | 'label_count' => _n_noop( |
| 617 | 688 | 'Pending Cheque <span class="count">(%s)</span>', |
| 618 | 689 | 'Pending Cheque <span class="count">(%s)</span>', |
| 619 | 690 | 'easy-invoice' |
| @@ -650,9 +721,9 @@ | ||
| 650 | 721 | $redirect_url = admin_url('admin.php?page=easy-quote-all&rewrite_flushed=1'); |
| 651 | 722 | } |
| 652 | 723 | } |
| 653 | 724 | |
| 654 | - wp_redirect($redirect_url); | |
| 725 | + wp_safe_redirect($redirect_url); | |
| 655 | 726 | exit; |
| 656 | 727 | } |
| 657 | 728 | |
| 658 | 729 | /** |
| @@ -680,9 +751,9 @@ | ||
| 680 | 751 | $redirect_url = admin_url('admin.php?page=easy-quote-all&post_types_registered=1'); |
| 681 | 752 | } |
| 682 | 753 | } |
| 683 | 754 | |
| 684 | - wp_redirect($redirect_url); | |
| 755 | + wp_safe_redirect($redirect_url); | |
| 685 | 756 | exit; |
| 686 | 757 | } |
| 687 | 758 | |
| 688 | 759 | /** |
| @@ -731,8 +802,9 @@ | ||
| 731 | 802 | 'easy-invoice-migration', // Migration page |
| 732 | 803 | 'easy-invoice-license', // License page |
| 733 | 804 | 'easy-invoice-free-vs-pro', |
| 734 | 805 | 'easy-invoice-join-community', |
| 806 | + 'easy-invoice-import', | |
| 735 | 807 | ]; |
| 736 | 808 | |
| 737 | 809 | // Dynamically include every addon's `settings_url` page slug. |
| 738 | 810 | // This means enterprise addons (time-tracking, dunning, white-label, |
| @@ -754,10 +826,27 @@ | ||
| 754 | 826 | } |
| 755 | 827 | $easy_invoice_pages = array_values( array_unique( $easy_invoice_pages ) ); |
| 756 | 828 | } |
| 757 | 829 | |
| 758 | - // Check if current page is an Easy Invoice page | |
| 759 | - if ( in_array( $page, $easy_invoice_pages ) ) { | |
| 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 ) { | |
| 760 | 849 | // Don't remove our review notice - keep it for free users |
| 761 | 850 | // Store our notice callback temporarily |
| 762 | 851 | $review_notice_callback = false; |
| 763 | 852 | $review_notice_priority = false; |