0, 'number' => '', 'text' => '', 'class' => 'easy-invoice-url', 'target' => '_blank' ], $atts, 'easy_invoice_url'); // Get invoice by ID or number $invoice = null; if (!empty($atts['id'])) { $invoice = \EasyInvoice\Providers\InvoiceServiceProvider::getInvoiceRepository()->find($atts['id']); } elseif (!empty($atts['number'])) { $invoice = \EasyInvoice\Providers\InvoiceServiceProvider::getInvoiceRepository()->findByNumber($atts['number']); } if (!$invoice) { return '' . __('Invoice not found', 'easy-invoice') . ''; } $url = get_permalink($invoice->getId()); // Only use secure link if enabled in settings and available $secure_links_enabled = get_option('easy_invoice_pro_enable_secure_links', 'no') === 'yes'; if ($secure_links_enabled && class_exists('\EasyInvoicePro\Addons\SecureLinks\Controllers\PermalinkController')) { $secure_url = \EasyInvoicePro\Addons\SecureLinks\Controllers\PermalinkController::getInvoiceSecureLinkUrl($invoice->getId()); if ($secure_url) { $url = $secure_url; } } // SECURITY: the per-invoice access token authorises manual-payment // submission for the named invoice. Two rules govern when it can // be attached to a shortcode-rendered URL: // // 1. NEVER mint a new token from this code path. The shortcode // is callable by anyone able to author rendered content // (Contributors previewing drafts, public template includes), // so auto-minting via invoiceAccessToken() here would let a // low-privileged actor create payment-auth tokens for any // invoice ID they can guess. We read existing tokens only. // 2. Even reading an existing token is gated. Only the admin // (manage_options) or a viewer who already passes the // ownership check (canSubmitPaymentForInvoice — which // itself accepts a token already presented via ?ik=) gets // the keyed URL. Everyone else gets the bare permalink. // // Legitimate flows still work: // * Admin sends invoice email -> EmailManager (server-trusted) // mints + embeds the token in the emailed URL directly. // * Client clicks the emailed link -> arrives with ?ik= in URL // -> canSubmitPaymentForInvoice passes -> shortcode renders // other invoice URLs on the page with the keyed form too. // * Admin embedding [easy_invoice_url] on an admin-context // page sees the keyed URL via the manage_options branch. $invoice_id_int = (int) $invoice->getId(); $invoice_access_token = ''; if (current_user_can('manage_options') || \EasyInvoice\Controllers\InvoiceController::canSubmitPaymentForInvoice($invoice_id_int, $invoice)) { $invoice_access_token = \EasyInvoice\Controllers\InvoiceController::invoiceAccessTokenIfExists($invoice_id_int); } if ($invoice_access_token !== '' && $url) { $url = add_query_arg('ik', $invoice_access_token, $url); } $text = !empty($atts['text']) ? $atts['text'] : $invoice->getNumber(); $class = esc_attr($atts['class']); $target = esc_attr($atts['target']); return sprintf( '%s', esc_url($url), $class, $target, esc_html($text) ); } /** * Render quote URL shortcode * * @param array $atts Shortcode attributes * @return string Rendered shortcode */ public function renderQuoteUrl($atts) { $atts = shortcode_atts([ 'id' => 0, 'number' => '', 'text' => '', 'class' => 'easy-quote-url', 'target' => '_blank' ], $atts, 'easy_quote_url'); // Get quote by ID or number $quote = null; if (!empty($atts['id'])) { $quote = \EasyInvoice\Providers\QuoteServiceProvider::getQuoteRepository()->find($atts['id']); } elseif (!empty($atts['number'])) { $quote = \EasyInvoice\Providers\QuoteServiceProvider::getQuoteRepository()->findByNumber($atts['number']); } if (!$quote) { return '' . __('Quote not found', 'easy-invoice') . ''; } $url = get_permalink($quote->getId()); $secure_links_enabled = get_option('easy_invoice_pro_enable_secure_links', 'no') === 'yes'; if ($secure_links_enabled && class_exists('\EasyInvoicePro\Addons\SecureLinks\Controllers\PermalinkController')) { $secure_url = \EasyInvoicePro\Addons\SecureLinks\Controllers\PermalinkController::getQuoteSecureLinkUrl($quote->getId()); if ($secure_url) { $url = $secure_url; } } // SECURITY (CVE-2026-9021 + follow-up): the per-quote access // token authorises Accept/Decline. Same two-rule policy as the // invoice shortcode above (see renderInvoiceUrl for full // rationale): // // 1. NEVER mint a new token from the shortcode path — readers // only. Trusted email composition (EmailManager) keeps // using quoteAccessToken() which mints on first send. // 2. Only disclose an existing token to an admin or a viewer // who already passes the ownership check (canActOnQuote). // Other viewers get the bare permalink — they can view // the quote but not Accept/Decline until they arrive via // a legitimately-emailed link. $quote_id_int = (int) $quote->getId(); $quote_access_token = ''; if (current_user_can('manage_options') || \EasyInvoice\Controllers\QuoteController::canActOnQuote($quote_id_int, $quote)) { $quote_access_token = \EasyInvoice\Controllers\QuoteController::quoteAccessTokenIfExists($quote_id_int); } if ($quote_access_token !== '' && $url) { $url = add_query_arg('qk', $quote_access_token, $url); } $text = !empty($atts['text']) ? $atts['text'] : $quote->getNumber(); $class = esc_attr($atts['class']); $target = esc_attr($atts['target']); return sprintf( '%s', esc_url($url), $class, $target, esc_html($text) ); } /** * Add shortcode help to the help tab */ public function addShortcodeHelp() { $screen = get_current_screen(); if ($screen && ($screen->id === 'easy-invoice_page_easy-invoice-settings' || $screen->id === 'edit-easy_invoice')) { $screen->add_help_tab([ 'id' => 'easy-invoice-shortcodes', 'title' => __('Shortcodes', 'easy-invoice'), 'content' => $this->getShortcodeHelpContent() ]); } } /** * Get shortcode help content * * @return string Help content */ private function getShortcodeHelpContent() { $content = '

' . __('Available Shortcodes', 'easy-invoice') . '

'; $content .= '

[easy_invoice_url]

'; $content .= '

' . __('Displays a link to an invoice.', 'easy-invoice') . '

'; $content .= '

' . __('Attributes:', 'easy-invoice') . '

'; $content .= ''; $content .= '

' . __('Examples:', 'easy-invoice') . '

'; $content .= '
[easy_invoice_url id="123" text="View Invoice"]
'; $content .= '
[easy_invoice_url number="INV-001" text="Click here to view"]
'; $content .= '
'; $content .= '

[easy_quote_url]

'; $content .= '

' . __('Displays a link to a quote.', 'easy-invoice') . '

'; $content .= '

' . __('Attributes:', 'easy-invoice') . '

'; $content .= ''; $content .= '

' . __('Examples:', 'easy-invoice') . '

'; $content .= '
[easy_quote_url id="456" text="View Quote"]
'; $content .= '
[easy_quote_url number="QT-001" text="Click here to view"]
'; return $content; } }