# easy-invoice/2.4.1/includes/Services/DocumentAccessNoticeService.php

Easy Invoice – Invoice Generator, PDF Quotes &amp; Payments, version 2.4.1. 159 lines.

- Page: https://pluginprobe.com/plugins/easy-invoice/2.4.1/code/includes/Services/DocumentAccessNoticeService.php
- Raw: https://pluginprobe.com/plugins/easy-invoice/2.4.1/raw/includes/Services/DocumentAccessNoticeService.php
- Modified: 2026-09-15T12:31:20+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.1/code/includes/Services/DocumentAccessNoticeService.php#L10-L20`.

```php
<?php
/**
 * One-time upgrade notice for the document access-key change.
 *
 * @package Easy_Invoice
 * @subpackage Services
 */

namespace EasyInvoice\Services;

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

/**
 * Tells site owners, once, that invoice and quote links now require an access key.
 *
 * Why this exists
 * ---------------
 * From 2.4.0 an invoice or quote is only rendered to someone holding a valid
 * per-document access key, an administrator, or the signed-in client the document
 * is bound to (see TemplateLoader::enforceDocumentAccess). That closes a real
 * exposure — previously anyone with, or guessing, a URL could read a customer's
 * name, address, line items and totals, including on draft invoices.
 *
 * Documents emailed before per-document keys existed carry no `?ik=` / `?qk=`.
 * Such a link now lands on a page (TemplateLoader::renderLinkRefreshPage) that
 * shows nothing of the document and offers to email a fresh keyed link to the
 * address it was issued to — so the customer is not stuck and the merchant need
 * not act. The notice tells the merchant this is what their customers will see.
 *
 * Only shown where it is useful:
 *   - sites that already have documents (a fresh install has no old links to break);
 *   - once, until dismissed.
 *
 * It deliberately hooks plain `admin_notices` rather than trying to survive
 * EasyInvoice::disableAdminNoticesOnEasyInvoicePages(), which clears that hook on
 * Easy Invoice's own screens. The Dashboard and Plugins screens are where someone
 * lands after updating, and those are unaffected.
 */
class DocumentAccessNoticeService {

    /** Option holding the notice state: absent | 'show' | 'dismissed'. */
    const OPTION = 'easy_invoice_doc_access_notice';

    /** AJAX action used by the dismiss button. */
    const AJAX_DISMISS = 'easy_invoice_dismiss_doc_access_notice';

    /**
     * Wire up the notice.
     *
     * @return void
     */
    public static function init() {
        add_action( 'admin_init', [ __CLASS__, 'maybeFlagUpgrade' ] );
        add_action( 'admin_notices', [ __CLASS__, 'render' ] );
        add_action( 'wp_ajax_' . self::AJAX_DISMISS, [ __CLASS__, 'ajaxDismiss' ] );
    }

    /**
     * Decide, once, whether this site needs the notice.
     *
     * @return void
     */
    public static function maybeFlagUpgrade() {
        if ( get_option( self::OPTION, '' ) !== '' ) {
            return;
        }

        // A site with no documents has no previously-sent links to break, so there is
        // nothing to warn about. Stamp it dismissed so this never runs again.
        update_option( self::OPTION, self::hasExistingDocuments() ? 'show' : 'dismissed', false );
    }

    /**
     * Are there any invoices or quotes on this site?
     *
     * @return bool
     */
    private static function hasExistingDocuments(): bool {
        $found = get_posts( [
            'post_type'        => [
                \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE,
                \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE,
            ],
            'post_status'      => 'any',
            'numberposts'      => 1,
            'fields'           => 'ids',
            'suppress_filters' => true,
            'no_found_rows'    => true,
        ] );

        return ! empty( $found );
    }

    /**
     * Output the notice.
     *
     * @return void
     */
    public static function render() {
        if ( ! current_user_can( 'manage_options' ) ) {
            return;
        }
        if ( get_option( self::OPTION, '' ) !== 'show' ) {
            return;
        }

        $nonce = wp_create_nonce( self::AJAX_DISMISS );
        ?>
        <div class="notice notice-warning is-dismissible" id="easy-invoice-doc-access-notice">
            <p>
                <strong><?php esc_html_e( 'Easy Invoice: invoice and quote links are now protected by an access key.', 'easy-invoice' ); ?></strong>
            </p>
            <p>
                <?php esc_html_e( 'Invoices and quotes used to be readable by anyone who had, or guessed, the URL — including drafts. They are now shown only to you, to the signed-in client the document belongs to, or to someone opening the link that was emailed to them.', 'easy-invoice' ); ?>
            </p>
            <p>
                <?php esc_html_e( 'Links you sent before this update carry no key. Anyone opening one sees no document data — just a page offering to email a fresh link to the address the document was issued to, one click, no action needed from you. Anything sent from now on is protected automatically.', 'easy-invoice' ); ?>
            </p>
            <?php // phpcs:ignore -- inline script keeps the notice self-contained. ?>
            <script>
            (function () {
                var el = document.getElementById('easy-invoice-doc-access-notice');
                if (!el) { return; }
                el.addEventListener('click', function (e) {
                    if (!e.target.classList.contains('notice-dismiss')) { return; }
                    var body = new FormData();
                    body.append('action', <?php echo wp_json_encode( self::AJAX_DISMISS ); ?>);
                    body.append('nonce', <?php echo wp_json_encode( $nonce ); ?>);
                    fetch(<?php echo wp_json_encode( admin_url( 'admin-ajax.php' ) ); ?>, {
                        method: 'POST', body: body, credentials: 'same-origin'
                    });
                });
            })();
            </script>
        </div>
        <?php
    }

    /**
     * Persist the dismissal.
     *
     * @return void
     */
    public static function ajaxDismiss() {
        $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
        if ( ! wp_verify_nonce( $nonce, self::AJAX_DISMISS ) ) {
            wp_send_json_error( [ 'message' => __( 'Security check failed', 'easy-invoice' ) ] );
        }
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( [ 'message' => __( 'You do not have permission to perform this action', 'easy-invoice' ) ] );
        }

        update_option( self::OPTION, 'dismissed', false );
        wp_send_json_success();
    }
}

```
