# easy-invoice/2.4.0/includes/Services/DocumentViews.php

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

- Page: https://pluginprobe.com/plugins/easy-invoice/2.4.0/code/includes/Services/DocumentViews.php
- Raw: https://pluginprobe.com/plugins/easy-invoice/2.4.0/raw/includes/Services/DocumentViews.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.0/code/includes/Services/DocumentViews.php#L10-L20`.

```php
<?php
/**
 * Records when a client opens an invoice or quote.
 *
 * @package Easy_Invoice
 * @subpackage Services
 */

namespace EasyInvoice\Services;

use EasyInvoice\Constants\PostTypes;

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

/**
 * "Has the client seen it?" is the question behind most chasing. Every
 * invoicing product a freelancer has used before answers it -- FreshBooks,
 * Zoho and GetPaid all show "viewed on" -- so its absence reads as a gap
 * even though nobody lists it as a feature.
 *
 * A view is recorded when the public document page is served to someone
 * who is not staff. Staff previews are ignored: the merchant opening their
 * own invoice is not the client reading it. The IP is kept with its last
 * octet (or IPv6 host part) zeroed, which is enough to tell "same office"
 * from "somewhere new" without holding personal data.
 */
class DocumentViews {

    const META_FIRST = '_easy_invoice_first_viewed';
    const META_LAST  = '_easy_invoice_last_viewed';
    const META_COUNT = '_easy_invoice_view_count';
    const META_IP    = '_easy_invoice_last_viewed_from';

    /**
     * Wire it up.
     *
     * @return void
     */
    public static function init(): void {
        // After enforceDocumentAccess (priority 1) and PdfDownload (5): if the
        // request reaches here the viewer was allowed to see the document.
        add_action( 'template_redirect', [ __CLASS__, 'maybeRecord' ], 8 );
    }

    /**
     * Record the view when the request is a client looking at a document.
     *
     * @return void
     */
    public static function maybeRecord(): void {
        if ( is_admin() || ! is_singular() ) {
            return;
        }
        $post = get_queried_object();
        if ( ! $post instanceof \WP_Post ) {
            return;
        }
        $types = [ PostTypes::EASY_INVOICE_POST_TYPE => 'invoice', PostTypes::EASY_INVOICE_QUOTE_POST_TYPE => 'quote' ];
        if ( ! isset( $types[ $post->post_type ] ) ) {
            return;
        }
        if ( self::isStaff() ) {
            return;
        }

        /**
         * Filter whether this request counts as the client viewing the document.
         *
         * @param bool     $record Default true.
         * @param \WP_Post $post   The document.
         */
        if ( ! apply_filters( 'easy_invoice_record_document_view', true, $post ) ) {
            return;
        }

        self::record( (int) $post->ID, $types[ $post->post_type ] );
    }

    /**
     * Write the view.
     *
     * @param int    $document_id Post id.
     * @param string $type        'invoice' or 'quote'.
     * @return void
     */
    public static function record( int $document_id, string $type ): void {
        $now   = current_time( 'mysql' );
        $first = (string) get_post_meta( $document_id, self::META_FIRST, true );
        $count = (int) get_post_meta( $document_id, self::META_COUNT, true );

        if ( '' === $first ) {
            update_post_meta( $document_id, self::META_FIRST, $now );
        }
        update_post_meta( $document_id, self::META_LAST, $now );
        update_post_meta( $document_id, self::META_COUNT, $count + 1 );
        update_post_meta( $document_id, self::META_IP, self::anonymisedIp() );

        /**
         * Fires when a client views an invoice or quote.
         *
         * @param int    $document_id Post id.
         * @param string $type        'invoice' or 'quote'.
         * @param bool   $first_view  True the first time.
         */
        do_action( 'easy_invoice_document_viewed', $document_id, $type, '' === $first );
    }

    /**
     * View summary for display.
     *
     * @param int $document_id Post id.
     * @return array{first:string,last:string,count:int,from:string}
     */
    public static function summary( int $document_id ): array {
        return [
            'first' => (string) get_post_meta( $document_id, self::META_FIRST, true ),
            'last'  => (string) get_post_meta( $document_id, self::META_LAST, true ),
            'count' => (int) get_post_meta( $document_id, self::META_COUNT, true ),
            'from'  => (string) get_post_meta( $document_id, self::META_IP, true ),
        ];
    }

    /**
     * Short human label: "Viewed 3× · 2 hours ago" or "Not viewed yet".
     *
     * @param int $document_id Post id.
     * @return string
     */
    public static function label( int $document_id ): string {
        $s = self::summary( $document_id );
        if ( 0 === $s['count'] || '' === $s['last'] ) {
            return __( 'Not viewed yet', 'easy-invoice' );
        }
        $ago = human_time_diff( strtotime( $s['last'] ), current_time( 'timestamp' ) );

        return 1 === $s['count']
            /* translators: %s: how long ago. */
            ? sprintf( __( 'Viewed %s ago', 'easy-invoice' ), $ago )
            /* translators: %1$d: number of views; %2$s: how long ago. */
            : sprintf( __( 'Viewed %1$d× · last %2$s ago', 'easy-invoice' ), $s['count'], $ago );
    }

    /**
     * Is the current user someone who works on invoices rather than pays them?
     *
     * @return bool
     */
    private static function isStaff(): bool {
        if ( ! is_user_logged_in() ) {
            return false;
        }
        if ( current_user_can( 'manage_options' ) ) {
            return true;
        }
        return function_exists( 'easy_invoice_user_can' ) && easy_invoice_user_can( 'ei_view_invoices' );
    }

    /**
     * The viewer's IP with the host part removed.
     *
     * @return string
     */
    private static function anonymisedIp(): string {
        $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
        if ( '' === $ip ) {
            return '';
        }
        if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
            $parts    = explode( '.', $ip );
            $parts[3] = '0';
            return implode( '.', $parts );
        }
        if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
            $packed = inet_pton( $ip );
            if ( false !== $packed ) {
                return (string) inet_ntop( substr( $packed, 0, 8 ) . str_repeat( "\0", 8 ) );
            }
        }
        return '';
    }
}

```
