PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.1
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.1
2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.2 All 57 releases
easy-invoice / includes / Services / DocumentViews.php

DocumentViews.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.4.1, at includes/Services/DocumentViews.php

184 lines 6.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Records when a client opens an invoice or quote.
4 *
5 * @package Easy_Invoice
6 * @subpackage Services
7 */
8
9 namespace EasyInvoice\Services;
10
11 use EasyInvoice\Constants\PostTypes;
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 /**
18 * "Has the client seen it?" is the question behind most chasing. Every
19 * invoicing product a freelancer has used before answers it -- FreshBooks,
20 * Zoho and GetPaid all show "viewed on" -- so its absence reads as a gap
21 * even though nobody lists it as a feature.
22 *
23 * A view is recorded when the public document page is served to someone
24 * who is not staff. Staff previews are ignored: the merchant opening their
25 * own invoice is not the client reading it. The IP is kept with its last
26 * octet (or IPv6 host part) zeroed, which is enough to tell "same office"
27 * from "somewhere new" without holding personal data.
28 */
29 class DocumentViews {
30
31 const META_FIRST = '_easy_invoice_first_viewed';
32 const META_LAST = '_easy_invoice_last_viewed';
33 const META_COUNT = '_easy_invoice_view_count';
34 const META_IP = '_easy_invoice_last_viewed_from';
35
36 /**
37 * Wire it up.
38 *
39 * @return void
40 */
41 public static function init(): void {
42 // After enforceDocumentAccess (priority 1) and PdfDownload (5): if the
43 // request reaches here the viewer was allowed to see the document.
44 add_action( 'template_redirect', [ __CLASS__, 'maybeRecord' ], 8 );
45 }
46
47 /**
48 * Record the view when the request is a client looking at a document.
49 *
50 * @return void
51 */
52 public static function maybeRecord(): void {
53 if ( is_admin() || ! is_singular() ) {
54 return;
55 }
56 $post = get_queried_object();
57 if ( ! $post instanceof \WP_Post ) {
58 return;
59 }
60 $types = [ PostTypes::EASY_INVOICE_POST_TYPE => 'invoice', PostTypes::EASY_INVOICE_QUOTE_POST_TYPE => 'quote' ];
61 if ( ! isset( $types[ $post->post_type ] ) ) {
62 return;
63 }
64 if ( self::isStaff() ) {
65 return;
66 }
67
68 /**
69 * Filter whether this request counts as the client viewing the document.
70 *
71 * @param bool $record Default true.
72 * @param \WP_Post $post The document.
73 */
74 if ( ! apply_filters( 'easy_invoice_record_document_view', true, $post ) ) {
75 return;
76 }
77
78 self::record( (int) $post->ID, $types[ $post->post_type ] );
79 }
80
81 /**
82 * Write the view.
83 *
84 * @param int $document_id Post id.
85 * @param string $type 'invoice' or 'quote'.
86 * @return void
87 */
88 public static function record( int $document_id, string $type ): void {
89 $now = current_time( 'mysql' );
90 $first = (string) get_post_meta( $document_id, self::META_FIRST, true );
91 $count = (int) get_post_meta( $document_id, self::META_COUNT, true );
92
93 if ( '' === $first ) {
94 update_post_meta( $document_id, self::META_FIRST, $now );
95 }
96 update_post_meta( $document_id, self::META_LAST, $now );
97 update_post_meta( $document_id, self::META_COUNT, $count + 1 );
98 update_post_meta( $document_id, self::META_IP, self::anonymisedIp() );
99
100 /**
101 * Fires when a client views an invoice or quote.
102 *
103 * @param int $document_id Post id.
104 * @param string $type 'invoice' or 'quote'.
105 * @param bool $first_view True the first time.
106 */
107 do_action( 'easy_invoice_document_viewed', $document_id, $type, '' === $first );
108 }
109
110 /**
111 * View summary for display.
112 *
113 * @param int $document_id Post id.
114 * @return array{first:string,last:string,count:int,from:string}
115 */
116 public static function summary( int $document_id ): array {
117 return [
118 'first' => (string) get_post_meta( $document_id, self::META_FIRST, true ),
119 'last' => (string) get_post_meta( $document_id, self::META_LAST, true ),
120 'count' => (int) get_post_meta( $document_id, self::META_COUNT, true ),
121 'from' => (string) get_post_meta( $document_id, self::META_IP, true ),
122 ];
123 }
124
125 /**
126 * Short human label: "Viewed 3× · 2 hours ago" or "Not viewed yet".
127 *
128 * @param int $document_id Post id.
129 * @return string
130 */
131 public static function label( int $document_id ): string {
132 $s = self::summary( $document_id );
133 if ( 0 === $s['count'] || '' === $s['last'] ) {
134 return __( 'Not viewed yet', 'easy-invoice' );
135 }
136 $ago = human_time_diff( strtotime( $s['last'] ), current_time( 'timestamp' ) );
137
138 return 1 === $s['count']
139 /* translators: %s: how long ago. */
140 ? sprintf( __( 'Viewed %s ago', 'easy-invoice' ), $ago )
141 /* translators: %1$d: number of views; %2$s: how long ago. */
142 : sprintf( __( 'Viewed %1$d× · last %2$s ago', 'easy-invoice' ), $s['count'], $ago );
143 }
144
145 /**
146 * Is the current user someone who works on invoices rather than pays them?
147 *
148 * @return bool
149 */
150 private static function isStaff(): bool {
151 if ( ! is_user_logged_in() ) {
152 return false;
153 }
154 if ( current_user_can( 'manage_options' ) ) {
155 return true;
156 }
157 return function_exists( 'easy_invoice_user_can' ) && easy_invoice_user_can( 'ei_view_invoices' );
158 }
159
160 /**
161 * The viewer's IP with the host part removed.
162 *
163 * @return string
164 */
165 private static function anonymisedIp(): string {
166 $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
167 if ( '' === $ip ) {
168 return '';
169 }
170 if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
171 $parts = explode( '.', $ip );
172 $parts[3] = '0';
173 return implode( '.', $parts );
174 }
175 if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
176 $packed = inet_pton( $ip );
177 if ( false !== $packed ) {
178 return (string) inet_ntop( substr( $packed, 0, 8 ) . str_repeat( "\0", 8 ) );
179 }
180 }
181 return '';
182 }
183 }
184