# easy-invoice/2.4.0/includes/Shortcodes/ShortcodeManager.php

Easy Invoice – Invoice Generator, PDF Quotes &amp; Payments, version 2.4.0. 247 lines.

- Page: https://pluginprobe.com/plugins/easy-invoice/2.4.0/code/includes/Shortcodes/ShortcodeManager.php
- Raw: https://pluginprobe.com/plugins/easy-invoice/2.4.0/raw/includes/Shortcodes/ShortcodeManager.php
- Modified: 2026-06-29T10:06:54+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/easy-invoice/2.4.0/code/includes/Shortcodes/ShortcodeManager.php#L10-L20`.

```php
<?php
/**
 * Shortcode Manager for Easy Invoice Free
 * 
 * Manages shortcodes for invoice and quote URLs
 *
 * @package EasyInvoice
 * @subpackage Shortcodes
 * @since 2.0.0
 */

namespace EasyInvoice\Shortcodes;

if (!defined('ABSPATH')) {
    exit;
}

/**
 * ShortcodeManager Class
 */
class ShortcodeManager {
    
    /**
     * Constructor
     */
    public function __construct() {
        // Register shortcodes
        add_shortcode('easy_invoice_url', [$this, 'renderInvoiceUrl']);
        add_shortcode('easy_quote_url', [$this, 'renderQuoteUrl']);
        
        // Add shortcode info to help tab
        add_action('admin_head', [$this, 'addShortcodeHelp']);
    }

    /**
     * Render invoice URL shortcode
     *
     * @param array $atts Shortcode attributes
     * @return string Rendered shortcode
     */
    public function renderInvoiceUrl($atts) {
        $atts = shortcode_atts([
            'id' => 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 '<span class="easy-invoice-error">' . __('Invoice not found', 'easy-invoice') . '</span>';
        }

        $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(
            '<a href="%s" class="%s" target="%s">%s</a>',
            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 '<span class="easy-invoice-error">' . __('Quote not found', 'easy-invoice') . '</span>';
        }

        $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(
            '<a href="%s" class="%s" target="%s">%s</a>',
            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 = '<h2>' . __('Available Shortcodes', 'easy-invoice') . '</h2>';
        
        $content .= '<h3><code>[easy_invoice_url]</code></h3>';
        $content .= '<p>' . __('Displays a link to an invoice.', 'easy-invoice') . '</p>';
        $content .= '<h4>' . __('Attributes:', 'easy-invoice') . '</h4>';
        $content .= '<ul>';
        $content .= '<li><code>id</code> - ' . __('Invoice ID (required if number is not provided)', 'easy-invoice') . '</li>';
        $content .= '<li><code>number</code> - ' . __('Invoice number (required if id is not provided)', 'easy-invoice') . '</li>';
        $content .= '<li><code>text</code> - ' . __('Link text (default: invoice number)', 'easy-invoice') . '</li>';
        $content .= '<li><code>class</code> - ' . __('CSS class for the link (default: easy-invoice-url)', 'easy-invoice') . '</li>';
        $content .= '<li><code>target</code> - ' . __('Link target (default: _blank)', 'easy-invoice') . '</li>';
        $content .= '</ul>';
        $content .= '<h4>' . __('Examples:', 'easy-invoice') . '</h4>';
        $content .= '<pre><code>[easy_invoice_url id="123" text="View Invoice"]</code></pre>';
        $content .= '<pre><code>[easy_invoice_url number="INV-001" text="Click here to view"]</code></pre>';
        
        $content .= '<hr>';
        
        $content .= '<h3><code>[easy_quote_url]</code></h3>';
        $content .= '<p>' . __('Displays a link to a quote.', 'easy-invoice') . '</p>';
        $content .= '<h4>' . __('Attributes:', 'easy-invoice') . '</h4>';
        $content .= '<ul>';
        $content .= '<li><code>id</code> - ' . __('Quote ID (required if number is not provided)', 'easy-invoice') . '</li>';
        $content .= '<li><code>number</code> - ' . __('Quote number (required if id is not provided)', 'easy-invoice') . '</li>';
        $content .= '<li><code>text</code> - ' . __('Link text (default: quote number)', 'easy-invoice') . '</li>';
        $content .= '<li><code>class</code> - ' . __('CSS class for the link (default: easy-quote-url)', 'easy-invoice') . '</li>';
        $content .= '<li><code>target</code> - ' . __('Link target (default: _blank)', 'easy-invoice') . '</li>';
        $content .= '</ul>';
        $content .= '<h4>' . __('Examples:', 'easy-invoice') . '</h4>';
        $content .= '<pre><code>[easy_quote_url id="456" text="View Quote"]</code></pre>';
        $content .= '<pre><code>[easy_quote_url number="QT-001" text="Click here to view"]</code></pre>';
        
        return $content;
    }
}

```
