PluginProbe ʕ •ᴥ•ʔ
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents / 4.6.0
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents v4.6.0
4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 trunk 1.0.0 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 2.0.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.2.0 2.2.1 2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.1.3 3.2.0 3.2.1 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.6.8 3.7.0 3.7.1 3.7.2 3.7.3 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.9.0 3.9.1 3.9.10 3.9.11 3.9.12 3.9.13 3.9.14 3.9.15 3.9.16 3.9.17 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9 4.0.0 4.0.1 4.0.10 4.0.11 4.0.12 4.0.13 4.0.14 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.10 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6 4.1.7 4.1.8 4.1.9 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7 4.2.8 4.2.9 4.3.0 4.3.1 4.4.0 4.4.1 4.4.10 4.4.11 4.4.2 4.4.3 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 4.5.0 4.5.1
embedpress / EmbedPress / Includes / Classes / View_Count_Display.php
embedpress / EmbedPress / Includes / Classes Last commit date
Analytics 2 weeks ago Database 2 months ago DynamicFieldResolver.php 1 month ago Elementor_Enhancer.php 6 months ago EmbedPress_Core_Installer.php 6 years ago EmbedPress_Notice.php 3 months ago EmbedPress_Plugin_Usage_Tracker.php 1 month ago Extend_CustomPlayer_Controls.php 2 months ago Extend_Elementor_Controls.php 1 year ago FeatureNoticeManager.php 4 days ago FeatureNotices.php 4 days ago FeaturePreviewModal.php 4 days ago Feature_Enhancer.php 1 month ago GoogleReviewsAdminPage.php 4 days ago GoogleReviewsApify.php 4 days ago GoogleReviewsManaged.php 4 days ago GoogleReviewsRenderer.php 4 days ago GoogleReviewsRestController.php 4 days ago GoogleReviewsStore.php 4 days ago Helper.php 4 days ago Pdf_Thumbnail_Handler.php 2 months ago PermalinkHelper.php 10 months ago View_Count_Display.php 2 weeks ago
View_Count_Display.php
358 lines
1 <?php
2
3 namespace EmbedPress\Includes\Classes;
4
5 use EmbedPress\Includes\Classes\Analytics\Data_Collector;
6
7 defined('ABSPATH') or die("No direct script access allowed.");
8
9 /**
10 * Public-facing PDF / Document view-count badge.
11 *
12 * Works uniformly across Gutenberg blocks, the [embedpress] shortcode, and the
13 * Elementor PDF / Document widgets — all of which already emit a wrapper with
14 * `data-embed-type="PDF"` or `data-embed-type="Document"`. The frontend script
15 * discovers those wrappers, fetches the per-embed view count from the REST
16 * endpoint registered here, and injects a `.ep-view-count` badge into the DOM.
17 *
18 * Reads from the existing analytics views table — no schema change.
19 */
20 class View_Count_Display
21 {
22 const REST_NAMESPACE = 'embedpress/v1';
23
24 /**
25 * Embed-type marker values we treat as PDF/document.
26 *
27 * Gutenberg blocks emit `PDF` / `Document`; the [embedpress] shortcode
28 * uses provider strings like `document_pdf`, `document_doc`,
29 * `document_ppt`, etc.; the Elementor PDF widget uses `PDF`.
30 */
31 public static function get_supported_types()
32 {
33 return apply_filters('embedpress_view_count_supported_types', [
34 'PDF',
35 'Document',
36 'document',
37 'document_pdf',
38 'document_doc',
39 'document_docx',
40 'document_ppt',
41 'document_pptx',
42 'document_xls',
43 'document_xlsx',
44 ]);
45 }
46
47 public static function register()
48 {
49 add_action('rest_api_init', [self::class, 'register_rest_route']);
50 add_action('wp_enqueue_scripts', [self::class, 'enqueue_assets']);
51 }
52
53 public static function register_rest_route()
54 {
55 register_rest_route(self::REST_NAMESPACE, '/analytics/view-count', [
56 'methods' => 'GET',
57 'permission_callback' => '__return_true',
58 'args' => [
59 'content_id' => [
60 'required' => true,
61 'sanitize_callback' => 'sanitize_text_field',
62 ],
63 ],
64 'callback' => [self::class, 'rest_get_view_count'],
65 ]);
66
67 register_rest_route(self::REST_NAMESPACE, '/analytics/view-count/track', [
68 'methods' => 'POST',
69 'permission_callback' => '__return_true',
70 'args' => [
71 'content_id' => [
72 'required' => true,
73 'sanitize_callback' => 'sanitize_text_field',
74 ],
75 'session_id' => [
76 'required' => false,
77 'sanitize_callback' => 'sanitize_text_field',
78 ],
79 'embed_url' => [
80 'required' => false,
81 'sanitize_callback' => 'esc_url_raw',
82 ],
83 'embed_type' => [
84 'required' => false,
85 'sanitize_callback' => 'sanitize_text_field',
86 ],
87 ],
88 'callback' => [self::class, 'rest_track_view'],
89 ]);
90
91 register_rest_route(self::REST_NAMESPACE, '/analytics/download-count', [
92 'methods' => 'GET',
93 'permission_callback' => '__return_true',
94 'args' => [
95 'content_id' => [
96 'required' => true,
97 'sanitize_callback' => 'sanitize_text_field',
98 ],
99 ],
100 'callback' => [self::class, 'rest_get_download_count'],
101 ]);
102
103 register_rest_route(self::REST_NAMESPACE, '/analytics/view-count/download', [
104 'methods' => 'POST',
105 'permission_callback' => '__return_true',
106 'args' => [
107 'content_id' => [
108 'required' => true,
109 'sanitize_callback' => 'sanitize_text_field',
110 ],
111 'session_id' => [
112 'required' => false,
113 'sanitize_callback' => 'sanitize_text_field',
114 ],
115 'embed_url' => [
116 'required' => false,
117 'sanitize_callback' => 'esc_url_raw',
118 ],
119 'embed_type' => [
120 'required' => false,
121 'sanitize_callback' => 'sanitize_text_field',
122 ],
123 ],
124 'callback' => [self::class, 'rest_track_download'],
125 ]);
126 }
127
128 public static function rest_get_download_count($request)
129 {
130 $content_id = (string) $request->get_param('content_id');
131 $count = (new Data_Collector())->get_download_count_by_content_id($content_id);
132
133 return rest_ensure_response([
134 'content_id' => $content_id,
135 'count' => (int) $count,
136 ]);
137 }
138
139 /**
140 * Self-record a download click. Mirrors rest_track_view but writes a row
141 * tagged as a download (interaction_type=click, source=download in JSON).
142 * Session-deduped per day so a single visitor clicking N times only
143 * registers once.
144 */
145 public static function rest_track_download($request)
146 {
147 // No global on/off switch: the counter is controlled solely by the
148 // per-embed "Show Download Count" toggle (data-ep-downloads="on"), which
149 // gates both the badge render and whether this endpoint is ever called.
150 global $wpdb;
151 $content_id = (string) $request->get_param('content_id');
152 $session_id = (string) ($request->get_param('session_id') ?: '');
153 $embed_url = (string) ($request->get_param('embed_url') ?: '');
154 $embed_type = (string) ($request->get_param('embed_type') ?: '');
155
156 if ($content_id === '' || $session_id === '') {
157 return new \WP_Error('embedpress_invalid_args', 'content_id and session_id are required', ['status' => 400]);
158 }
159
160 $table = $wpdb->prefix . 'embedpress_analytics_views';
161
162 // Every download click increments the badge — record one row per
163 // download rather than deduping per session/day. A download is an
164 // intentional action, so downloading the same file again counts again.
165 $wpdb->insert($table, [
166 'content_id' => $content_id,
167 'session_id' => $session_id,
168 'user_id' => $session_id,
169 'interaction_type' => 'click',
170 'user_ip' => self::get_ip(),
171 'page_url' => isset($_SERVER['HTTP_REFERER']) ? esc_url_raw(wp_unslash($_SERVER['HTTP_REFERER'])) : '',
172 'interaction_data' => wp_json_encode([
173 'embed_type' => $embed_type,
174 'embed_url' => $embed_url,
175 'source' => 'download',
176 ]),
177 'created_at' => current_time('mysql'),
178 ]);
179
180 $count = (new Data_Collector())->get_download_count_by_content_id($content_id);
181 return rest_ensure_response([
182 'recorded' => true,
183 'content_id' => $content_id,
184 'count' => (int) $count,
185 ]);
186 }
187
188 public static function rest_get_view_count($request)
189 {
190 $content_id = (string) $request->get_param('content_id');
191 // Badge shows only its own (visitor_view_count) rows so the analytics
192 // tracker's scroll-driven view rows can't inflate it.
193 $count = (new Data_Collector())->get_visitor_view_count_by_content_id($content_id);
194
195 return rest_ensure_response([
196 'content_id' => $content_id,
197 'count' => (int) $count,
198 ]);
199 }
200
201 /**
202 * Self-record a 'view' row for the visitor-view-count badge.
203 *
204 * Writes to the existing analytics views table so the count is consistent
205 * with what the analytics dashboard sees, but the write path is independent
206 * of `embedpress_analytics_tracking_enabled` — the badge feature has its
207 * own toggle and is allowed to grow its count even when the dashboard
208 * tracker is disabled.
209 *
210 * Per-day per-session dedup avoids double-counting when the analytics
211 * tracker is ALSO active for the same session.
212 */
213 public static function rest_track_view($request)
214 {
215 // No global on/off switch: the counter is controlled solely by the
216 // per-embed "Show View Count" toggle (data-ep-views="on"), which gates
217 // both the badge render and whether this endpoint is ever called.
218 global $wpdb;
219 $content_id = (string) $request->get_param('content_id');
220 $session_id = (string) ($request->get_param('session_id') ?: '');
221 $embed_url = (string) ($request->get_param('embed_url') ?: '');
222 $embed_type = (string) ($request->get_param('embed_type') ?: '');
223
224 if ($content_id === '' || $session_id === '') {
225 return new \WP_Error('embedpress_invalid_args', 'content_id and session_id are required', ['status' => 400]);
226 }
227
228 $table = $wpdb->prefix . 'embedpress_analytics_views';
229
230 // Every view increments the badge — record one row per request rather
231 // than deduping per session/day. The badge is a raw view tally (hit
232 // counter), so a repeat visit or refresh counts again.
233 $wpdb->insert($table, [
234 'content_id' => $content_id,
235 'session_id' => $session_id,
236 'user_id' => $session_id,
237 'interaction_type' => 'view',
238 'user_ip' => self::get_ip(),
239 'page_url' => isset($_SERVER['HTTP_REFERER']) ? esc_url_raw(wp_unslash($_SERVER['HTTP_REFERER'])) : '',
240 'interaction_data' => wp_json_encode([
241 'embed_type' => $embed_type,
242 'embed_url' => $embed_url,
243 'source' => 'visitor_view_count',
244 ]),
245 'created_at' => current_time('mysql'),
246 ]);
247
248 $count = (new Data_Collector())->get_visitor_view_count_by_content_id($content_id);
249 return rest_ensure_response([
250 'recorded' => true,
251 'content_id' => $content_id,
252 'count' => (int) $count,
253 ]);
254 }
255
256 private static function get_ip()
257 {
258 $ip = $_SERVER['REMOTE_ADDR'] ?? '';
259 return is_string($ip) ? substr(sanitize_text_field($ip), 0, 45) : '';
260 }
261
262 /**
263 * Does the current singular post/page contain an embed that opts INTO the
264 * counter at the per-embed level (data-ep-views="on" / data-ep-downloads="on")?
265 * This lets a single embed show its badge even when the global option is off,
266 * which is the whole point of the per-embed toggle. Elementor pages store the
267 * markup in post meta, not post_content, so we scan a serialized snapshot of
268 * the post when available.
269 */
270 private static function post_has_optin()
271 {
272 if (!is_singular()) {
273 return false;
274 }
275 $post = get_post();
276 if (!$post) {
277 return false;
278 }
279 $haystack = (string) $post->post_content;
280 // Gutenberg saves the rendered data-ep-* attributes into post_content.
281 if (strpos($haystack, 'data-ep-views="on"') !== false
282 || strpos($haystack, 'data-ep-downloads="on"') !== false) {
283 return true;
284 }
285
286 // Elementor stores SETTINGS (not rendered HTML) in _elementor_data, so
287 // detect the widget toggle keys there — the rendered data-ep-views="on"
288 // attribute doesn't exist until render time. Covers PDF + Document
289 // widgets (embedpress_pdf_* / embedpress_doc_*).
290 $elementor = get_post_meta($post->ID, '_elementor_data', true);
291 if (is_string($elementor) && $elementor !== '') {
292 foreach ([
293 'embedpress_pdf_show_view_count',
294 'embedpress_pdf_show_download_count',
295 'embedpress_doc_show_view_count',
296 'embedpress_doc_show_download_count',
297 ] as $key) {
298 // Matches "<key>":"yes" with optional JSON backslash-escaping.
299 if (preg_match('/' . preg_quote($key, '/') . '\\\\?"\s*:\s*\\\\?"yes/', $elementor)) {
300 return true;
301 }
302 }
303 // Also catch already-rendered attributes if present.
304 if (strpos($elementor, 'data-ep-views\\"on') !== false
305 || strpos($elementor, 'data-ep-downloads\\"on') !== false) {
306 return true;
307 }
308 }
309
310 return false;
311 }
312
313 public static function enqueue_assets()
314 {
315 if (is_admin()) {
316 return;
317 }
318 // Rendering is driven solely by the per-embed toggle, so ship the asset
319 // only when the current page has an embed that opts in
320 // (data-ep-views="on" / data-ep-downloads="on"). The global option no
321 // longer renders a badge on its own.
322 if (!self::post_has_optin()) {
323 return;
324 }
325
326 wp_enqueue_script(
327 'embedpress-view-count',
328 plugins_url('assets/js/ep-view-count.js', EMBEDPRESS_FILE),
329 [],
330 EMBEDPRESS_VERSION,
331 true
332 );
333
334 wp_localize_script('embedpress-view-count', 'embedpressViewCount', [
335 'restUrl' => esc_url_raw(rest_url(self::REST_NAMESPACE . '/analytics/view-count')),
336 'trackUrl' => esc_url_raw(rest_url(self::REST_NAMESPACE . '/analytics/view-count/track')),
337 'downloadUrl' => esc_url_raw(rest_url(self::REST_NAMESPACE . '/analytics/download-count')),
338 'downloadTrackUrl' => esc_url_raw(rest_url(self::REST_NAMESPACE . '/analytics/view-count/download')),
339 // Count-badge positioning is a Pro feature — only the default
340 // ('below') placement is free. The frontend ignores any Pro
341 // position when Pro is inactive so the badge falls back to default.
342 'isPro' => Helper::is_pro_active(),
343 'types' => array_values(self::get_supported_types()),
344 'labels' => [
345 /* translators: %s: formatted number of views */
346 'singular' => __('%s view', 'embedpress'),
347 /* translators: %s: formatted number of views */
348 'plural' => __('%s views', 'embedpress'),
349 'downloadButton' => __('Download', 'embedpress'),
350 /* translators: %s: formatted number of downloads */
351 'downloadSingular' => __('%s download', 'embedpress'),
352 /* translators: %s: formatted number of downloads */
353 'downloadPlural' => __('%s downloads', 'embedpress'),
354 ],
355 ]);
356 }
357 }
358