PluginProbe
Copy Anything to Clipboard for WordPress – Copy Button, Copy Text & Copy Code / 5.5.4
Copy Anything to Clipboard for WordPress – Copy Button, Copy Text & Copy Code v5.5.4
5.5.4 5.5.3 3.1.0 3.2.0 3.2.1 3.3.0 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.6.0 3.7.0 3.8.0 3.8.1 3.8.2 3.8.3 4.0.0 4.0.2 4.0.3 4.0.4 4.0.5 4.1.0 All 79 releases
copy-the-code / includes / global-injector / class-frontend.php

class-frontend.php in Copy Anything to Clipboard for WordPress – Copy Button, Copy Text & Copy Code 5.5.4, at includes/global-injector/class-frontend.php

549 lines 16.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Global Injector Frontend
4 *
5 * Handles frontend rendering and script loading for Global Injector rules.
6 *
7 * @package CTC
8 * @since 5.0.0
9 */
10
11 namespace CTC\Global_Injector;
12
13 use CTC\Helper;
14 use CTC\Global_Injector\Display_Conditions;
15 use CTC\Global_Injector\Inline_CSS;
16 use CTC\Global_Injector\Styles\Button as ButtonStyle;
17 use CTC\Global_Injector\Styles\Icon as IconStyle;
18 use CTC\Global_Injector\Styles\Cover as CoverStyle;
19
20 /**
21 * Frontend
22 *
23 * @since 5.0.0
24 */
25 class Frontend {
26
27 /**
28 * Instance
29 *
30 * @since 5.0.0
31 *
32 * @access private
33 * @var object Class object.
34 */
35 private static $instance;
36
37 /**
38 * Initiator
39 *
40 * @since 5.0.0
41 *
42 * @return object initialized object of class.
43 */
44 public static function get() {
45 if ( null === self::$instance ) {
46 self::$instance = new self();
47 }
48 return self::$instance;
49 }
50
51 /**
52 * Constructor
53 *
54 * @since 5.0.0
55 */
56 public function __construct() {
57 add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ] );
58 add_action( 'admin_bar_menu', [ $this, 'add_admin_bar_links' ], 100 );
59 }
60
61 /**
62 * Enqueue frontend assets
63 *
64 * @since 5.0.0
65 * @return void
66 */
67 public function enqueue_assets() {
68 $rules = $this->get_rules_for_page();
69 if ( empty( $rules ) ) {
70 return;
71 }
72
73 // Enqueue CTC lib core library (CopyEngine, CapabilityDetector).
74 wp_enqueue_script(
75 'ctc-lib-core',
76 CTC_URI . 'assets/frontend/js/lib/ctc.js',
77 [],
78 CTC_VER,
79 true
80 );
81
82 // Enqueue Global Injector frontend script.
83 wp_enqueue_script(
84 'ctc-global-injector-frontend',
85 CTC_URI . 'assets/frontend/js/global-injector.js',
86 [ 'ctc-lib-core' ],
87 CTC_VER,
88 true
89 );
90
91 // Inline CSS: only for needed styles/positions, minified, filterable. No separate CSS file.
92 $needed = $this->get_needed_styles_and_positions( $rules );
93 $raw_css = Inline_CSS::build( $needed['styles'], $needed['positions'] );
94 $min_css = Helper::minify_css( $raw_css );
95 $inline_css = (string) apply_filters( 'ctc/global_injector/inline_css', $min_css );
96
97 if ( $inline_css !== '' ) {
98 wp_register_style( 'ctc-global-injector-inline', false, [], CTC_VER );
99 wp_enqueue_style( 'ctc-global-injector-inline' );
100 wp_add_inline_style( 'ctc-global-injector-inline', $inline_css );
101 }
102
103 // Localize script with rules and page context.
104 $localize_data = $this->get_localize_data( $rules );
105 /**
106 * Filter frontend localize data.
107 *
108 * Pro can use this to add eventsUrl for analytics tracking.
109 *
110 * @since 5.3.0
111 * @param array $data Localized data array.
112 * @param array $rules Active rules for the current page.
113 */
114 $localize_data = apply_filters( 'ctc/global_injector/frontend_localize_data', $localize_data, $rules );
115
116 wp_localize_script(
117 'ctc-global-injector-frontend',
118 'ctcGlobalInjector',
119 $localize_data
120 );
121 }
122
123 /**
124 * Get localize data for frontend script
125 *
126 * @since 5.0.0
127 * @param array $rules Active rules.
128 * @return array
129 */
130 private function get_localize_data( $rules ) {
131 return [
132 'rules' => $rules,
133 'apiUrl' => rest_url( 'ctc/v1/rules' ),
134 'eventsUrl' => rest_url( 'ctc/v1/analytics/events' ),
135 'isPro' => Helper::is_pro(),
136 'postId' => get_the_ID() ? get_the_ID() : null,
137 'postType' => get_post_type() ? get_post_type() : null,
138 'pageUrl' => $this->get_current_page_url(),
139 'pageContext' => Display_Conditions::get_page_context(),
140 'debug' => (bool) apply_filters( 'ctc/global_injector/debug', defined( 'WP_DEBUG' ) && WP_DEBUG ),
141 'styles' => [
142 'button' => ButtonStyle::get_localize_data(),
143 'icon' => IconStyle::get_localize_data(),
144 'cover' => CoverStyle::get_localize_data(),
145 ],
146 ];
147 }
148
149 /**
150 * Get rules that should load on the current page (single source of truth).
151 *
152 * Used for enqueue_assets, admin bar, and localized data. No scripts or CSS
153 * are enqueued when this returns an empty array.
154 *
155 * @since 5.0.0
156 * @return array List of rule data arrays for rules that pass display conditions.
157 */
158 public function get_rules_for_page() {
159 $rules = $this->get_active_rules();
160 return apply_filters( 'ctc/global_injector/rules_for_page', $rules );
161 }
162
163 /**
164 * Get which visual styles and positions are used by the given rules.
165 *
166 * Used to inject only the CSS needed for the current page.
167 *
168 * @since 5.0.2
169 * @param array $rules Rule data arrays (from get_rules_for_page).
170 * @return array{styles: string[], positions: string[]}
171 */
172 private function get_needed_styles_and_positions( array $rules ) {
173 $styles = [];
174 $positions = [];
175 $valid_positions = [
176 'inside_top_left',
177 'inside_top_right',
178 'inside_bottom_left',
179 'inside_bottom_right',
180 'outside_before_left',
181 'outside_before_right',
182 'outside_after_left',
183 'outside_after_right',
184 ];
185
186 foreach ( $rules as $rule ) {
187 $style = isset( $rule['visual_style'] ) ? $rule['visual_style'] : 'button';
188 if ( ! in_array( $style, $styles, true ) ) {
189 $styles[] = $style;
190 }
191 $pos = isset( $rule['button_position'] ) ? $rule['button_position'] : 'inside_top_right';
192 if ( in_array( $pos, $valid_positions, true ) && ! in_array( $pos, $positions, true ) ) {
193 $positions[] = $pos;
194 }
195 }
196
197 if ( empty( $positions ) ) {
198 $positions = [ 'inside_top_right' ];
199 }
200
201 return [
202 'styles' => array_values( $styles ),
203 'positions' => array_values( $positions ),
204 ];
205 }
206
207 /**
208 * Get current page URL
209 *
210 * @since 5.0.0
211 * @return string|null
212 */
213 private function get_current_page_url() {
214 if ( is_singular() ) {
215 return get_permalink();
216 }
217
218 if ( is_front_page() ) {
219 return home_url( '/' );
220 }
221
222 // Fallback to current URL.
223 global $wp;
224 return home_url( add_query_arg( [], $wp->request ) );
225 }
226
227 /**
228 * Add admin bar links to edit Global Injector rules that apply on this page.
229 *
230 * Shows clipboard icon + "CTC" parent with "Edit: [Rule name]" children for each active rule.
231 *
232 * @since 5.0.0
233 * @param \WP_Admin_Bar $wp_admin_bar Admin bar object.
234 * @return void
235 */
236 public function add_admin_bar_links( $wp_admin_bar ) {
237 if ( is_admin() || ! current_user_can( 'manage_options' ) ) {
238 return;
239 }
240
241 $rules = $this->get_rules_for_page();
242 if ( empty( $rules ) ) {
243 return;
244 }
245
246 $base_url = admin_url( 'options-general.php?page=ctc-global-injector' );
247 $parent_id = 'ctc';
248 $title = __( 'CTC', 'ctc' );
249
250 $wp_admin_bar->add_node(
251 [
252 'id' => $parent_id,
253 'title' => '<span class="ab-icon dashicons dashicons-clipboard" style="top: 4px;font-size: 18px;"></span><span class="ab-label">' . $title . '</span>',
254 'href' => $base_url,
255 'meta' => [
256 'title' => __( 'Copy the Code – edit rules on this page', 'ctc' ),
257 ],
258 ]
259 );
260
261 foreach ( $rules as $rule ) {
262 $rule_id = isset( $rule['id'] ) ? (int) $rule['id'] : 0;
263 $rule_title = $rule_id ? get_the_title( $rule_id ) : __( 'Untitled Rule', 'ctc' );
264 $edit_url = add_query_arg( 'rule', $rule_id, $base_url );
265
266 $wp_admin_bar->add_node(
267 [
268 'parent' => $parent_id,
269 'id' => $parent_id . '-rule-' . $rule_id,
270 'title' => sprintf( /* translators: %s: rule name */ __( 'Edit: %s', 'ctc' ), $rule_title ),
271 'href' => $edit_url,
272 'meta' => [
273 'title' => sprintf( /* translators: %s: rule name */ __( 'Edit rule "%s" in Global Injector', 'ctc' ), $rule_title ),
274 ],
275 ]
276 );
277 }
278 }
279
280 /**
281 * Get active rules for frontend
282 *
283 * @since 5.0.0
284 * @return array
285 */
286 private function get_active_rules() {
287 $posts = get_posts(
288 [
289 'post_type' => 'copy-to-clipboard',
290 'posts_per_page' => -1,
291 'post_status' => 'any',
292 'orderby' => 'date',
293 'order' => 'DESC',
294 ]
295 );
296
297 $rules = [];
298 foreach ( $posts as $post ) {
299 // Check if rule is active.
300 $is_active = get_post_meta( $post->ID, 'is_active', true );
301 if ( '' === $is_active ) {
302 $is_active = 'publish' === $post->post_status;
303 } else {
304 $is_active = '0' !== $is_active && false !== $is_active;
305 }
306
307 // Skip inactive rules.
308 if ( ! $is_active ) {
309 continue;
310 }
311
312 // Get display conditions.
313 $display_conditions = get_post_meta( $post->ID, 'display_conditions', true );
314
315 // Evaluate display conditions.
316 if ( ! empty( $display_conditions ) && is_array( $display_conditions ) ) {
317 $should_display = Display_Conditions::evaluate( $display_conditions );
318 if ( ! $should_display ) {
319 continue;
320 }
321 }
322
323 // Build rule data.
324 $rule = $this->build_rule_data( $post );
325
326 if ( $rule ) {
327 /**
328 * Filter the frontend rule data before adding to the rules array.
329 *
330 * Pro add-on can use this to add additional fields like custom_css_class.
331 *
332 * @since 5.0.0
333 * @param array $rule Rule data array.
334 * @param \WP_Post $post Post object.
335 */
336 $rules[] = apply_filters( 'ctc/global_injector/frontend_rule_data', $rule, $post );
337 }
338 }
339
340 return $rules;
341 }
342
343 /**
344 * Build rule data for frontend
345 *
346 * @since 5.0.0
347 * @param \WP_Post $post Post object.
348 * @return array|null
349 */
350 private function build_rule_data( $post ) {
351 $selector = get_post_meta( $post->ID, 'selector', true );
352
353 // Skip rules without selectors.
354 if ( empty( $selector ) ) {
355 return null;
356 }
357
358 $visual_style_raw = get_post_meta( $post->ID, 'visual_style', true );
359 $visual_style = ! empty( $visual_style_raw ) ? $visual_style_raw : 'button';
360
361 // Get style configuration based on visual style.
362 $style_config = $this->get_style_config( $post->ID, $visual_style );
363
364 return [
365 'id' => $post->ID,
366 'selector' => $selector,
367 'exclude_selector' => get_post_meta( $post->ID, 'exclude_selector', true ),
368 'visual_style' => $visual_style,
369 'button_text' => $this->get_meta_with_default( $post->ID, 'button-text', __( 'Copy', 'ctc' ) ),
370 'success_text' => $this->get_meta_with_default( $post->ID, 'button-copy-text', __( 'Copied!', 'ctc' ) ),
371 'tooltip_text' => $this->get_meta_with_default( $post->ID, 'tooltip-text', __( 'Copy to clipboard', 'ctc' ) ),
372 'button_position' => $this->migrate_position( $this->get_meta_with_default( $post->ID, 'button-position', 'inside_top_right' ) ),
373 'copy_format' => $this->get_meta_with_default( $post->ID, 'copy-format', 'text' ),
374 'copy_as' => $this->get_copy_as_for_rule( $post->ID ),
375 'analytics_enabled' => $this->get_bool_meta( $post->ID, 'analytics_enabled', true ),
376 'icon_enabled' => $this->get_bool_meta( $post->ID, 'icon_enabled', true ),
377 'icon_position' => $this->get_meta_with_default( $post->ID, 'icon_position', 'left' ),
378 'icon_key' => $this->get_meta_with_default( $post->ID, 'icon_key', 'clipboard' ),
379 'style' => $style_config,
380 ];
381 }
382
383 /**
384 * Get style configuration for a rule
385 *
386 * @since 5.0.0
387 * @param int $post_id Post ID.
388 * @param string $visual_style Visual style type.
389 * @return array
390 */
391 private function get_style_config( $post_id, $visual_style ) {
392 switch ( $visual_style ) {
393 case 'button':
394 return [
395 'text_color' => $this->get_meta_with_default( $post_id, 'text_color', '#ffffff' ),
396 'bg_color' => $this->get_meta_with_default( $post_id, 'bg_color', '#4f46e5' ),
397 'border_radius' => $this->get_int_meta_with_default( $post_id, 'border_radius', 6 ),
398 'font_size' => $this->get_int_meta_with_default( $post_id, 'font_size', 13 ),
399 'padding_x' => $this->get_int_meta_with_default( $post_id, 'padding_x', 16 ),
400 'padding_y' => $this->get_int_meta_with_default( $post_id, 'padding_y', 8 ),
401 ];
402
403 case 'icon':
404 return [
405 'icon_color' => $this->get_meta_with_default( $post_id, 'icon_color', '#6b7280' ),
406 'icon_hover_color' => $this->get_meta_with_default( $post_id, 'icon_hover_color', '#4f46e5' ),
407 'bg_color' => $this->get_meta_with_default( $post_id, 'bg_color', 'transparent' ),
408 'border_color' => $this->get_meta_with_default( $post_id, 'border_color', '#e5e7eb' ),
409 'icon_size' => $this->get_int_meta_with_default( $post_id, 'icon_size', 16 ),
410 'padding' => $this->get_int_meta_with_default( $post_id, 'padding', 8 ),
411 'border_radius' => $this->get_int_meta_with_default( $post_id, 'border_radius', 6 ),
412 'border_width' => $this->get_int_meta_with_default( $post_id, 'border_width', 1 ),
413 ];
414
415 case 'cover':
416 return [
417 'overlay_color' => $this->get_meta_with_default( $post_id, 'overlay_color', '#0f172a' ),
418 'hover_overlay_color' => $this->get_meta_with_default( $post_id, 'hover_overlay_color', '#4f46e5' ),
419 'overlay_opacity' => $this->get_int_meta_with_default( $post_id, 'overlay_opacity', 20 ),
420 'blur' => $this->get_int_meta_with_default( $post_id, 'blur', 2 ),
421 'cover_text_color' => $this->get_meta_with_default( $post_id, 'cover_text_color', '#0f172a' ),
422 'cover_bg_color' => $this->get_meta_with_default( $post_id, 'cover_bg_color', '#ffffff' ),
423 'cover_hover_bg_color' => $this->get_meta_with_default( $post_id, 'cover_hover_bg_color', '#f1f5f9' ),
424 'cover_border_radius' => $this->get_int_meta_with_default( $post_id, 'cover_border_radius', 9999 ),
425 'cover_font_size' => $this->get_int_meta_with_default( $post_id, 'cover_font_size', 10 ),
426 'cover_padding_x' => $this->get_int_meta_with_default( $post_id, 'cover_padding_x', 12 ),
427 'cover_padding_y' => $this->get_int_meta_with_default( $post_id, 'cover_padding_y', 6 ),
428 ];
429
430 default:
431 return [];
432 }
433 }
434
435 /**
436 * Migrate legacy position values to new format
437 *
438 * Old wp.org plugin used 'inside' and 'outside' values.
439 * New format uses specific positions like 'inside_top_right'.
440 *
441 * @since 5.0.0
442 * @param string $position Position value (may be legacy or new).
443 * @return string Migrated position value.
444 */
445 private function migrate_position( $position ) {
446 // Legacy to new position mapping.
447 $legacy_map = [
448 'inside' => 'inside_top_right',
449 'outside' => 'outside_after_right',
450 ];
451
452 // All valid new position values.
453 $valid_positions = [
454 'inside_top_left',
455 'inside_top_right',
456 'inside_bottom_left',
457 'inside_bottom_right',
458 'outside_before_left',
459 'outside_before_right',
460 'outside_after_left',
461 'outside_after_right',
462 ];
463
464 // If already a valid new position, return as-is.
465 if ( in_array( $position, $valid_positions, true ) ) {
466 return $position;
467 }
468
469 // If legacy value, migrate it.
470 if ( isset( $legacy_map[ $position ] ) ) {
471 return $legacy_map[ $position ];
472 }
473
474 // Default fallback.
475 return 'inside_top_right';
476 }
477
478 /**
479 * Get clipboard as for a rule (for Copy as: text, html, image, etc.).
480 * Uses copy-as meta when set; otherwise derives from legacy copy-format.
481 *
482 * @since 5.0.0
483 * @param int $post_id Post ID.
484 * @return string One of: text, html, text_and_html, image, json, svg.
485 */
486 private function get_copy_as_for_rule( $post_id ) {
487 $copy_as = get_post_meta( $post_id, 'copy-as', true );
488 $valid_types = [ 'text', 'html', 'text_and_html', 'image', 'json', 'svg' ];
489 if ( ! empty( $copy_as ) && in_array( $copy_as, $valid_types, true ) ) {
490 return $copy_as;
491 }
492 $copy_format = get_post_meta( $post_id, 'copy-format', true );
493 if ( 'html' === $copy_format || 'innerHTML' === $copy_format ) {
494 return 'html';
495 }
496 if ( 'text' === $copy_format ) {
497 return 'text';
498 }
499 return 'text_and_html';
500 }
501
502 /**
503 * Get post meta with default value
504 *
505 * @since 5.0.0
506 * @param int $post_id Post ID.
507 * @param string $key Meta key.
508 * @param mixed $fallback Default value.
509 * @return mixed
510 */
511 private function get_meta_with_default( $post_id, $key, $fallback ) {
512 $value = get_post_meta( $post_id, $key, true );
513 return ! empty( $value ) ? $value : $fallback;
514 }
515
516 /**
517 * Get integer post meta with default value
518 *
519 * @since 5.0.0
520 * @param int $post_id Post ID.
521 * @param string $key Meta key.
522 * @param int $fallback Default value.
523 * @return int
524 */
525 private function get_int_meta_with_default( $post_id, $key, $fallback ) {
526 $value = get_post_meta( $post_id, $key, true );
527 return ! empty( $value ) ? (int) $value : $fallback;
528 }
529
530 /**
531 * Get boolean meta value
532 *
533 * @since 5.0.0
534 * @param int $post_id Post ID.
535 * @param string $key Meta key.
536 * @param bool $fallback Default value.
537 * @return bool
538 */
539 private function get_bool_meta( $post_id, $key, $fallback = false ) {
540 $value = get_post_meta( $post_id, $key, true );
541
542 if ( '' === $value ) {
543 return $fallback;
544 }
545
546 return '1' === $value || true === $value;
547 }
548 }
549