PluginProbe
Print, PDF & Email by PrintFriendly / 5.5.13
Print, PDF & Email by PrintFriendly v5.5.13
5.5.13 5.5.12 5.5.11 trunk 0.2 0.9 1.0 2.0 2.1.8 3.0 3.0.9 3.1.6 3.10.0 3.11.0 3.11.1 3.11.2 3.12.0 3.12.1 3.12.2 3.12.3 3.12.4 3.12.5 3.13.0 3.14.0 3.14.1 All 95 releases
printfriendly / pf.php

pf.php in Print, PDF & Email by PrintFriendly 5.5.13, at pf.php

1,957 lines 78.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php // phpcs:ignore PSR1.Files.SideEffects.FoundWithSymbols
2
3 /*
4 Plugin Name: Print, PDF & Email by PrintFriendly
5 Plugin URI: https://www.printfriendly.com
6 Description: PrintFriendly & PDF button for your website. Optimizes your pages and brand for print, pdf, and email.
7 Name and URL are included to ensure repeat visitors and new visitors when printed versions are shared.
8 Version: 5.5.13
9 Author: Print, PDF, & Email by PrintFriendly
10 Author URI: https://www.printfriendly.com
11 License: GPLv2 or later
12 License URI: https://www.gnu.org/licenses/gpl-2.0.html
13 Domain Path: /languages
14 Text Domain: printfriendly
15 */
16
17 if (! defined('ABSPATH')) {
18 exit; // Exit if accessed directly.
19 }
20
21 // phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
22 // phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps
23 // phpcs:disable PSR2.Classes.PropertyDeclaration.VarUsed
24 // phpcs:disable PSR2.Classes.PropertyDeclaration.ScopeMissing
25 // phpcs:disable Squiz.Scope.MethodScope.Missing
26 // phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
27
28 /**
29 * PrintFriendly WordPress plugin. Allows easy embedding of printfriendly.com buttons.
30 *
31 * @package PrintFriendly_WordPress
32 * @author PrintFriendly <support@printfriendly.com>
33 * @copyright Copyright (C) 2023, PrintFriendly
34 */
35
36 if (! class_exists('PrintFriendly_WordPress')) {
37 /**
38 * Class containing all the plugins functionality.
39 *
40 * @package PrintFriendly_WordPress
41 */
42 class PrintFriendly_WordPress
43 {
44 /**
45 * Current plugin version.
46 *
47 * @var string
48 */
49 var $plugin_version = '5.5.13';
50 /**
51 * The hook, used for text domain as well as hooks on pages and in get requests for admin.
52 *
53 * @var string
54 */
55 var $hook = 'printfriendly';
56 /**
57 * The option name, used throughout to refer to the plugins option and option group.
58 *
59 * @var string
60 */
61 var $option_name = 'printfriendly_option';
62 /**
63 * The plugins options, loaded on init containing all the plugins settings.
64 *
65 * @var array
66 */
67 var $options = array();
68 /**
69 * Database version, used to allow for easy upgrades to / additions in plugin options between plugin versions.
70 *
71 * @var int
72 */
73 var $db_version = 22;
74 /**
75 * Settings page, used within the plugin to reliably load the plugins admin JS and CSS files only on the admin page.
76 *
77 * @var string
78 */
79 var $settings_page = '';
80 /**
81 * Pro card v2 controller. Only set when PRINTFRIENDLY_PRO_CARD_V2 is on;
82 * null means the legacy views/pro.php card is in use.
83 *
84 * @var PrintFriendly_Pro_Card|null
85 */
86 var $pro_card = null;
87 /**
88 * List of all buttons with their attributes.
89 *
90 * @var array
91 */
92 private static $_buttons = array( // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
93 'buttons/printfriendly-pdf-email-button.png' => array( 'width' => 170, 'height' => 24 ),
94 'buttons/printfriendly-pdf-email-button-md.png' => array( 'width' => 194, 'height' => 30 ),
95 'buttons/printfriendly-pdf-email-button-notext.png' => array( 'width' => 110, 'height' => 30 ),
96 'buttons/printfriendly-pdf-button.png' => array( 'width' => 112, 'height' => 24 ),
97 'buttons/printfriendly-pdf-button-nobg.png' => array( 'width' => 112, 'height' => 24 ),
98 'buttons/printfriendly-pdf-button-nobg-md.png' => array( 'width' => 124, 'height' => 30 ),
99 'buttons/printfriendly-button.png' => array( 'width' => 112, 'height' => 24 ),
100 'buttons/printfriendly-button-nobg.png' => array( 'width' => 112, 'height' => 24 ),
101 'buttons/printfriendly-button-md.png' => array( 'width' => 124, 'height' => 30 ),
102 'buttons/printfriendly-button-lg.png' => array( 'width' => 154, 'height' => 28 ),
103 'buttons/print-button.png' => array( 'width' => 66, 'height' => 24 ),
104 'buttons/print-button-nobg.png' => array( 'width' => 66, 'height' => 24 ),
105 'buttons/print-button-gray.png' => array( 'width' => 66, 'height' => 24 ),
106 'icons/printfriendly-icon-sm.png' => array( 'width' => 17, 'height' => 17 ),
107 'icons/printfriendly-icon-md.png' => array( 'width' => 16, 'height' => 16 ),
108 'icons/printfriendly-icon-lg.png' => array( 'width' => 25, 'height' => 25 ),
109 );
110
111 /**
112 * Constructor
113 *
114 * @since 3.0
115 */
116 function __construct()
117 {
118 define('PRINTFRIENDLY_BASEPATH', dirname(__FILE__));
119 define('PRINTFRIENDLY_BASEURL', plugins_url('/', __FILE__));
120 // Retrieve the plugin options
121 $this->options = get_option($this->option_name);
122 // If the options array is empty, set defaults
123 if (! is_array($this->options)) {
124 $this->set_defaults();
125 }
126
127 /**
128 * Set page content selection option "WordPress Standard/Strict" to "WP Template"
129 */
130 if (isset($this->options['pf_algo']) && $this->options['pf_algo'] === 'ws') {
131 $this->options['pf_algo'] = 'wp';
132 update_option($this->option_name, $this->options);
133 }
134
135 // If the version number doesn't match, upgrade
136 if ($this->db_version > $this->options['db_version']) {
137 $this->upgrade();
138 }
139
140 $this->public_hooks();
141 if (is_admin()) {
142 $this->admin_hooks();
143 }
144 }
145
146 /**
147 * All public hooks.
148 */
149 function public_hooks()
150 {
151 add_action('wp_enqueue_scripts', array( $this, 'wp_enqueue_scripts' ));
152 add_action('wp_head', array(&$this, 'front_head'));
153
154 // automaticaly add the link
155 add_filter('the_content', array(&$this, 'show_link_on_content'));
156 add_filter('the_excerpt', array(&$this, 'show_link_on_excerpt'));
157
158 add_action('the_content', array(&$this, 'add_pf_content_class_around_content_hook'));
159 }
160
161 /**
162 * Load front end dependencies.
163 */
164 function wp_enqueue_scripts()
165 {
166 wp_enqueue_script('jquery');
167 }
168
169 /**
170 * All admin hooks.
171 */
172 function admin_hooks()
173 {
174 // Hook into init for registration of the option and the language files
175 add_action('admin_init', array(&$this, 'init'));
176 // Register the settings page
177 add_action('admin_menu', array(&$this, 'add_config_page'));
178 // Enqueue the needed scripts and styles
179 add_action('admin_enqueue_scripts', array(&$this, 'admin_enqueue_scripts'));
180 // Register a link to the settings page on the plugins overview page
181 add_filter('plugin_action_links', array(&$this, 'filter_plugin_actions'), 10, 2);
182 add_filter('plugin_row_meta', array(&$this, 'additional_links'), 10, 2);
183 add_filter('wp_dropdown_cats', array( &$this, 'wp_dropdown_cats_multiple' ), 10, 2);
184
185 // Pro card v2, dark launched behind PRINTFRIENDLY_PRO_CARD_V2. Off by default,
186 // in which case nothing below runs and views/pro.php renders as before.
187 require_once PRINTFRIENDLY_BASEPATH . '/includes/class-printfriendly-pro-card.php';
188 if (PrintFriendly_Pro_Card::enabled()) {
189 $this->pro_card = new PrintFriendly_Pro_Card($this->plugin_version, $this->hook, __FILE__);
190 }
191 }
192
193 /**
194 * Adds multiple select support for category dropdown.
195 *
196 * @since 4.2
197 **/
198 function wp_dropdown_cats_multiple($output, $attributes)
199 {
200 if (isset($attributes['multiple']) && $attributes['multiple']) {
201 $output = preg_replace('/^<select/i', '<select multiple', $output);
202 $output = str_replace("name='{$attributes['name']}'", "name='{$attributes['name']}[]'", $output);
203 foreach (array_map('trim', explode(',', $attributes['selected'])) as $value) {
204 $output = str_replace("value=\"{$value}\"", "value=\"{$value}\" selected", $output);
205 }
206 }
207 return $output;
208 }
209
210 /**
211 * Adds wraps content in pf-content class to help Printfriendly algo determine the content
212 *
213 * @since 3.2.8
214 **/
215 function add_pf_content_class_around_content_hook($content = false)
216 {
217 if ($this->is_enabled() && $this->is_wp_algo_on($content)) {
218 add_action('wp_footer', array(&$this, 'print_script_footer'));
219 return '<div class="pf-content">' . $content . '</div>';
220 } else {
221 return $content;
222 }
223 }
224
225 /**
226 * Override to check if print-only command is being used
227 *
228 * @since 3.3.0
229 **/
230 function print_only_override($content)
231 {
232 $pattern = '/class\s*?=\s*?(["\']|["\']([^"\']*?)\s)print-only(["\']|\s([^"\']*?)["\'])/';
233 $pf_pattern = '/class\s*?=\s*?(["\']|["\']([^"\']*?)\s)pf-content(["\']|\s([^"\']*?)["\'])/';
234 return ( preg_match($pattern, $content) || preg_match($pf_pattern, $content) );
235 }
236
237 /**
238 * Check if WP Algorithm is selected and content doesn't use print-only
239 *
240 * @since 3.5.4
241 **/
242 function is_wp_algo_on($content)
243 {
244 return ! class_exists('WooCommerce') && $content && $this->getVal('pf_algo') === 'wp' && ! $this->print_only_override($content);
245 }
246
247 /**
248 * PHP 4 Compatible Constructor
249 *
250 * @since 3.0
251 */
252 function PrintFriendly_WordPress()
253 {
254 $this->__construct();
255 }
256
257 /**
258 * Check if button should be visible on particular page
259 */
260 function is_enabled()
261 {
262 if (
263 ( is_page() && 'on' === $this->getVal('show_on_pages') )
264 || ( is_home() && 'on' === $this->getVal('show_on_homepage') )
265 || ( is_tax() && 'on' === $this->getVal('show_on_taxonomies') && $this->category_included() )
266 || ( is_category() && 'on' === $this->getVal('show_on_categories') && $this->category_included() )
267 || ( is_single() && 'on' === $this->getVal('show_on_posts') && $this->category_included() )
268 ) {
269 return true;
270 }
271 return false;
272 }
273
274 /**
275 * Determines the CSS rules to return for the `printfriendly` class.
276 *
277 * @since 5.2.5
278 */
279 function get_css_rules_for_button()
280 {
281 $css = array( 'z-index: 1000' );
282
283 if ($this->getVal('button_alignment_method') === 'default') {
284 $css[] = 'display: flex';
285 $css[] = sprintf(
286 'margin: %dpx %dpx %dpx %dpx',
287 $this->getVal('margin_top', 0),
288 $this->getVal('margin_right', 0),
289 $this->getVal('margin_bottom', 0),
290 $this->getVal('margin_left', 0)
291 );
292 } else {
293 $css[] = 'position: relative';
294 }
295
296 return implode('; ', $css);
297 }
298
299 /**
300 * Determines the CSS rules to return for the front end.
301 *
302 * @since 5.2.5
303 */
304 function get_main_css()
305 {
306 if ($this->getVal('button_alignment_method') === 'css') {
307 return '
308 @media print {
309 .printfriendly {
310 display: none;
311 }
312 }
313 ';
314 }
315
316 return sprintf(
317 '
318 @media screen {
319 .printfriendly {
320 %s
321 }
322 .printfriendly a, .printfriendly a:link, .printfriendly a:visited, .printfriendly a:hover, .printfriendly a:active {
323 font-weight: 600;
324 cursor: pointer;
325 text-decoration: none;
326 border: none;
327 -webkit-box-shadow: none;
328 -moz-box-shadow: none;
329 box-shadow: none;
330 outline:none;
331 font-size: %dpx !important;
332 color: %s !important;
333 }
334 .printfriendly.pf-alignleft {
335 %s
336 }
337 .printfriendly.pf-alignright {
338 %s
339 }
340 .printfriendly.pf-aligncenter {
341 justify-content: center;
342 %s
343 }
344 }
345
346 .pf-button-img {
347 border: none;
348 -webkit-box-shadow: none;
349 -moz-box-shadow: none;
350 box-shadow: none;
351 padding: 0;
352 margin: 0;
353 display: inline;
354 vertical-align: middle;
355 }
356
357 img.pf-button-img + .pf-button-text {
358 margin-left: 6px;
359 }
360
361 @media print {
362 .printfriendly {
363 display: none;
364 }
365 }
366 ',
367 $this->get_css_rules_for_button(),
368 $this->getVal('text_size'),
369 $this->getVal('text_color'),
370 $this->getVal('button_alignment_method') === 'default' ? 'justify-content: start;' : 'float: left;',
371 $this->getVal('button_alignment_method') === 'default' ? 'justify-content: end;' : 'float: right;',
372 $this->getVal('button_alignment_method') === 'default' ? '' : 'display: flex; align-items: center;'
373 );
374 }
375
376 /**
377 * Returns the custom CSS (including the style tag) for content positioning.
378 *
379 * @since 5.2.5
380 */
381 function get_content_position_css_tag()
382 {
383 $css = $this->getVal('content_position_css');
384 if (! empty($css)) {
385 // Defense in depth: strip any HTML tags (e.g. a stored "</style>"
386 // breakout) before echoing into the <style> element. wp_strip_all_tags()
387 // leaves bare ">" characters intact, so CSS child combinators keep working.
388 $css = wp_strip_all_tags($css);
389 return sprintf('<style type="text/css" id="pf-button-css">%s</style>', $css);
390 }
391 return null;
392 }
393
394 /**
395 * Prints the PrintFriendly button CSS, in the header.
396 *
397 * @since 3.0
398 */
399 function front_head()
400 {
401 if (! $this->is_enabled()) {
402 return;
403 }
404
405 if ($this->getVal('enable_css') !== 'on') {
406 return;
407 }
408 ?>
409 <style type="text/css" id="pf-main-css">
410 <?php
411 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Inline CSS assembled from integer- and hex-validated option values.
412 echo $this->get_main_css();
413 ?>
414 </style>
415
416 <?php
417 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Returns a <style> tag whose CSS is run through wp_strip_all_tags() inside the method.
418 echo $this->get_content_position_css_tag();
419 ?>
420
421 <style type="text/css" id="pf-excerpt-styles">
422 .pf-button.pf-button-excerpt {
423 display: none;
424 }
425 </style>
426
427 <?php
428 }
429
430 /**
431 * Prints the PrintFriendly JavaScript, in the footer, and loads it asynchronously.
432 *
433 * @since 3.0
434 */
435 function print_script_footer()
436 {
437 $tagline = $this->getVal('tagline');
438 $image_url = $this->getVal('image_url');
439 if ($this->getVal('logo') === 'favicon') {
440 $tagline = '';
441 $image_url = '';
442 }
443
444 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Returns markup whose dynamic attributes are escaped with esc_attr() inside the method.
445 echo $this->getSelectorsFromCustomCSS();
446
447 // Currently we use v3 for both: normal and password protected sites
448 ?>
449 <script type="text/javascript" id="pf_script">
450 <?php
451 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Static inline analytics JavaScript; contains no user input.
452 echo $this->get_analytics_code();
453 ?>
454 var pfHeaderImgUrl = '<?php echo esc_js(esc_url($image_url)); ?>';
455 var pfHeaderTagline = '<?php echo esc_js($tagline); ?>';
456 var pfdisableClickToDel = '<?php echo esc_js($this->getVal('click_to_delete')); ?>';
457 var pfImagesSize = '<?php echo esc_js($this->getVal('images-size')); ?>';
458 var pfImageDisplayStyle = '<?php echo esc_js($this->getVal('image-style')); ?>';
459 var pfEncodeImages = '<?php echo esc_js($this->getVal('password_protected') === 'yes' ? 1 : 0); ?>';
460 var pfShowHiddenContent = '<?php echo esc_js($this->getVal('show_hidden_content') === 'yes' ? 1 : 0); ?>';
461 var pfDisableEmail = '<?php echo esc_js($this->getVal('email')); ?>';
462 var pfDisablePDF = '<?php echo esc_js($this->getVal('pdf')); ?>';
463 var pfDisablePrint = '<?php echo esc_js($this->getVal('print')); ?>';
464
465 <?php
466 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Returns a JS variable whose value is escaped via esc_js( esc_url() ) inside the method.
467 echo $this->get_custom_css_js_var();
468 ?>
469
470 var pfPlatform = 'WordPress';
471
472 (function($){
473 $(document).ready(function(){
474 if($('.pf-button-content').length === 0){
475 $('style#pf-excerpt-styles').remove();
476 }
477 });
478 })(jQuery);
479 </script>
480 <?php
481 // $ver is null on purpose, and Plugin Check's MissingVersion warning
482 // here is a false positive: null appends no query string, while false
483 // would append WordPress's own version to a third-party CDN URL we do
484 // not control or version. The CDN handles its own cache busting.
485 wp_enqueue_script('printfriendly-sdk', 'https://cdn.printfriendly.com/printfriendly.js', array(), null, true);
486 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Returns a custom element whose content is escaped via esc_html() inside the method.
487 echo $this->get_custom_css_tag();
488 ?>
489
490 <?php
491 }
492
493 /**
494 * Used as a filter for the_content.
495 *
496 * @since ?
497 *
498 * @param string $content The content of the post, when the function is used as a filter.
499 *
500 * @return string The content with the button added to the content.
501 */
502 function show_link_on_content($content)
503 {
504 $content = $this->show_link($content, true);
505 return $content;
506 }
507
508 /**
509 * Used as a filter for the_excerpt.
510 *
511 * @since ?
512 *
513 * @param string $content The content of the post, when the function is used as a filter.
514 *
515 * @return string The content with the button added to the content.
516 */
517 function show_link_on_excerpt($content)
518 {
519 return $this->show_link($content, false);
520 }
521
522 /**
523 * Used to generate the the final content as per the required placement.
524 *
525 * @since 3.0
526 *
527 * @param string $content The content of the post, when the function is used as a filter.
528 * @param bool $on_content True if showing on the_content and False if showing on the_excerpt.
529 *
530 * @return string $button or $content with the button added to the content when appropriate, just the content when button shouldn't be added or just button when called manually.
531 */
532 private function show_link($content = false, $on_content = true)
533 {
534 if ($this->is_enabled() && $content) {
535 $button = $this->getButton(false, $on_content ? 'pf-button-content' : 'pf-button-excerpt');
536 // Hook the script call now, so it only get's loaded when needed, and need is determined by the user calling pf_button
537 add_action('wp_footer', array(&$this, 'print_script_footer'));
538 if ($this->getVal('content_placement') === 'before') {
539 return $button . $content;
540 } else {
541 return $content . $button;
542 }
543 }
544 return $content;
545 }
546
547 /**
548 * Returns the analytics function to be included in the front end.
549 *
550 * @since 5.1
551 *
552 * @return string
553 */
554 private function get_analytics_code()
555 {
556 $return = null;
557 if ($this->google_analytics_enabled()) {
558 $code = apply_filters(
559 'printfriendly_analytics_code',
560 "
561 if(typeof(_gaq) === 'function') {
562 _gaq.push(['_trackEvent','PRINTFRIENDLY', 'print', title]);
563 }else if(typeof(ga) === 'function') {
564 ga('send', 'event','PRINTFRIENDLY', 'print', title);
565 }else if(typeof(gtag) === 'function') {
566 gtag('event', 'printfriendly_button_click', {'event_category': 'printfriendly', 'event_label': title})
567 }else if(typeof(dataLayer) === 'object') {
568 dataLayer.push({
569 'event': 'printfriendly_button_click',
570 'pageTitle': title
571 })
572 }
573 "
574 );
575 $return = "
576 function pfTrackEvent(title) {
577 $code
578 }
579 ";
580 }
581 return $return;
582 }
583
584 /**
585 * Returns the HTML of the button.
586 *
587 * This can be called publicly from `pf_show_link()`.
588 *
589 * @since 3.3.8
590 *
591 * @param bool $add_footer_script Whether to add the script in the footer.
592 * @param string $add_class Additional class for the HTML button div.
593 * @param bool $print_whole_page Whether to print the whole page or follow default behaviour.
594 *
595 * @return Printfriendly Button HTML
596 */
597 public function getButton($add_footer_script = false, $add_class = '', $print_whole_page = false)
598 {
599 if ($add_footer_script) {
600 add_action('wp_footer', array(&$this, 'print_script_footer'));
601 }
602
603 $onclick = array();
604 $href = '#';
605
606 if ($print_whole_page) {
607 $onclick[] = 'window.print()';
608 } else {
609 if (is_singular()) {
610 $onclick[] = 'window.print()';
611 } else {
612 $href = add_query_arg('pfstyle', 'wp', get_permalink());
613 }
614 }
615
616 if ($this->google_analytics_enabled()) {
617 $onclick[] = "pfTrackEvent('" . esc_attr(get_the_title()) . "')";
618 }
619
620 $align_class = '';
621 $position = $this->getVal('content_position');
622
623 // none option has been removed and translates to left
624 if ('none' === $position) {
625 $position = 'left';
626 }
627 $align_class = 'pf-align' . $position;
628 $button = sprintf(
629 '<div class="printfriendly pf-button %s %s">
630 <a href="%s" rel="nofollow" onclick="%s" title="Printer Friendly, PDF & Email">
631 %s
632 </a>
633 </div>',
634 sanitize_html_class($add_class),
635 sanitize_html_class($align_class),
636 esc_attr(esc_url($href)),
637 ! empty($onclick) ? esc_attr(implode('; ', $onclick)) . '; return false;' : '',
638 $this->button()
639 );
640 $button = apply_filters('printfriendly_button', $button);
641 return $button;
642 }
643
644 /**
645 * Returns the custom selector CSS to be added.
646 *
647 * @since 5.0
648 * @returns string
649 */
650 private function getSelectorsFromCustomCSS()
651 {
652 $css = '';
653 if ($this->getVal('pf_algo') === 'css') {
654 $attributes = array();
655 $names = array( 'author', 'date', 'content', 'title', 'primaryImage' );
656 $data = '';
657 foreach ($names as $name) {
658 $selector = $this->getVal("css-{$name}");
659 $suffix = $name === 'content' ? 'Selectors' : 'Selector';
660 if (! empty($selector)) {
661 $data .= "{$name}{$suffix}=$selector;";
662 }
663 }
664 $fallback = $this->getVal('pf_algo_css_content');
665 if ($data) {
666 $css = sprintf('<printfriendly-options data-selectors="%s" data-fallback-strategy="%s"></printfriendly-options>', esc_attr($data), esc_attr($fallback));
667 }
668 }
669 return $css;
670 }
671
672
673 /**
674 * Checks if GA is enabled.
675 *
676 * @since 3.2.9
677 * @returns if google analytics enabled
678 */
679 function google_analytics_enabled()
680 {
681 return $this->getVal('enable_google_analytics') === 'yes';
682 }
683
684 /**
685 * Filter posts by category.
686 *
687 * @since 3.2.2
688 * @return boolean true if post belongs to category selected for button display
689 */
690 function category_included()
691 {
692 return !empty($this->getVal('show_on_cat')) ? in_category(explode(',', $this->getVal('show_on_cat'))) : true;
693 }
694
695 /**
696 * Register the textdomain and the options array along with the validation function
697 *
698 * @since 3.0
699 */
700 function init()
701 {
702 // Translations for WordPress.org-hosted plugins load automatically since WP 4.6; no manual textdomain loading is required.
703 // Register our option array
704 register_setting($this->option_name, $this->option_name, array( 'sanitize_callback' => array(&$this, 'options_validate') ));
705 }
706
707 /**
708 * Validate the saved options.
709 *
710 * @since 3.0
711 * @param array $input with unvalidated options.
712 * @return array $valid_input with validated options.
713 */
714 function options_validate($input)
715 {
716 // Prevent CSRF attack
717 check_admin_referer('pf-options', 'pf-nonce');
718 $valid_input = $input;
719
720 // Section 1 options
721 if (
722 ! isset($input['button_type']) || ! in_array(
723 $input['button_type'],
724 array(
725 'buttons/printfriendly-pdf-email-button.png',
726 'buttons/printfriendly-pdf-email-button-md.png',
727 'buttons/printfriendly-pdf-email-button-notext.png', // buttongroup1
728 'buttons/printfriendly-pdf-button.png',
729 'buttons/printfriendly-pdf-button-nobg.png',
730 'buttons/printfriendly-pdf-button-nobg-md.png', // buttongroup2
731 'buttons/printfriendly-button.png',
732 'buttons/printfriendly-button-nobg.png',
733 'buttons/printfriendly-button-md.png',
734 'buttons/printfriendly-button-lg.png', // buttongroup3
735 'buttons/print-button.png',
736 'buttons/print-button-nobg.png',
737 'buttons/print-button-gray.png', // buttongroup3
738 'custom-button', // custom
739 ),
740 true
741 )
742 ) {
743 $valid_input['button_type'] = 'printfriendly-pdf-button.png';
744 }
745
746 if (
747 ! isset($input['custom_button_icon']) || ! in_array(
748 $input['custom_button_icon'],
749 array(
750 'https://cdn.printfriendly.com/icons/printfriendly-icon-sm.png',
751 'https://cdn.printfriendly.com/icons/printfriendly-icon-md.png',
752 'https://cdn.printfriendly.com/icons/printfriendly-icon-lg.png',
753 'custom-image',
754 'no-image',
755 ),
756 true
757 )
758 ) {
759 $valid_input['custom_button_icon'] = 'https://cdn.printfriendly.com/icons/printfriendly-icon-md.png';
760 }
761
762 // if a custom image is not being chosen, reset it in case it existed in the past.
763 if (isset($valid_input['custom_button_icon']) && $valid_input['custom_button_icon'] !== 'custom-image') {
764 $valid_input['custom_image'] = '';
765 }
766
767 // @todo custom image url validation
768 if (! isset($input['custom_image']) || empty($input['custom_image'])) {
769 $valid_input['custom_image'] = '';
770 }
771
772 if (! isset($input['custom_button_text'])) {
773 $valid_input['custom_button_text'] = 'custom-text';
774 }
775
776 // @todo validate optional custom text
777 if (! isset($input['custom_text'])) {
778 $valid_input['custom_text'] = 'Print Friendly';
779 } else {
780 $valid_input['custom_text'] = $this->esc_html_if_needed($valid_input['custom_text']);
781 }
782
783 // Custom button selected, but no url nor text given, reset button type to default
784 if ('custom-button' === $valid_input['button_type'] && ( '' === $valid_input['custom_image'] && '' === $input['custom_text'] )) {
785 $valid_input['button_type'] = 'buttons/printfriendly-pdf-button.png';
786 add_settings_error($this->option_name, 'invalid_custom_image', __('No valid custom image url received, please enter a valid url to use a custom image.', 'printfriendly'));
787 }
788
789 $valid_input['text_size'] = (int) $input['text_size'];
790 if (! isset($valid_input['text_size']) || 0 === $valid_input['text_size']) {
791 $valid_input['text_size'] = 14;
792 } elseif (25 < $valid_input['text_size'] || 9 > $valid_input['text_size']) {
793 $valid_input['text_size'] = 14;
794 add_settings_error($this->option_name, 'invalid_text_size', __('The text size you entered is invalid, please stay between 9px and 25px', 'printfriendly'));
795 }
796
797 if (! isset($input['text_color'])) {
798 $valid_input['text_color'] = $this->getVal('text_color');
799 } elseif (! preg_match('/^#[a-f0-9]{3,6}$/i', $input['text_color'])) {
800 // Revert to previous setting and throw error.
801 $valid_input['text_color'] = $this->getVal('text_color');
802 add_settings_error($this->option_name, 'invalid_color', __('The color you entered is not valid, it must be a valid hexadecimal RGB font color.', 'printfriendly'));
803 }
804
805 /* Section 2 options */
806 if (! isset($input['enable_css']) || 'off' !== $input['enable_css']) {
807 $valid_input['enable_css'] = 'on';
808 }
809
810 if (! isset($input['content_position']) || ! in_array($input['content_position'], array('left', 'center', 'right'), true)) {
811 $valid_input['content_position'] = 'left';
812 }
813
814 if (! isset($input['button_alignment_method']) || ! in_array($input['button_alignment_method'], array('default', 'css'), true)) {
815 $valid_input['button_alignment_method'] = 'default';
816 }
817
818 if (! isset($input['button_alignment_method']) || 'css' !== $input['button_alignment_method']) {
819 $valid_input['content_position_css'] = '';
820 } elseif (isset($input['content_position_css'])) {
821 // Strip all HTML/PHP tags (including any "</style>" breakout) before
822 // storing, mirroring the Custom CSS sanitization below. A bare ">"
823 // (CSS child combinator) is preserved because strip_tags() only removes
824 // complete "<...>" sequences.
825 $position_css = wp_strip_all_tags($input['content_position_css']);
826 $valid_input['content_position_css'] = sanitize_textarea_field($position_css);
827 } else {
828 $valid_input['content_position_css'] = '';
829 }
830
831 if (! isset($input['content_placement']) || ! in_array($input['content_placement'], array('before', 'after'), true)) {
832 $valid_input['content_placement'] = 'after';
833 }
834
835 foreach (array('margin_top', 'margin_right', 'margin_bottom', 'margin_left') as $opt) {
836 // if margin is not defined, don't throw a PHP notice
837 if (isset($input[ $opt ])) {
838 $valid_input[ $opt ] = (int) $input[ $opt ];
839 }
840 }
841
842 unset($opt);
843
844 /* Section 3 options */
845 foreach (array('show_on_posts', 'show_on_pages', 'show_on_homepage', 'show_on_categories', 'show_on_taxonomies') as $opt) {
846 if (! isset($input[ $opt ]) || 'on' !== $input[ $opt ]) {
847 unset($valid_input[ $opt ]);
848 }
849 }
850 unset($opt);
851
852 if (isset($input['category_ids'])) {
853 unset($valid_input['category_ids']);
854 }
855
856 /* Section 4 options */
857 if (! isset($input['logo']) || ! in_array($input['logo'], array('favicon', 'upload-an-image'), true)) {
858 $valid_input['logo'] = 'favicon';
859 }
860
861 // @todo custom logo url validation
862 if (! isset($input['image_url']) || empty($input['image_url'])) {
863 $valid_input['image_url'] = '';
864 }
865
866 // @todo validate optional tagline text
867 if (! isset($input['tagline'])) {
868 $valid_input['tagline'] = '';
869 }
870
871 // Custom logo selected, but no valid url given, reset logo to default
872 if ('upload-an-image' === $valid_input['logo'] && '' === $valid_input['image_url']) {
873 $valid_input['logo'] = 'favicon';
874 add_settings_error($this->option_name, 'invalid_custom_logo', __('No valid custom logo url received, please enter a valid url to use a custom logo.', 'printfriendly'));
875 }
876
877 if (! isset($input['image-style']) || ! in_array($input['image-style'], array('right', 'left', 'none', 'block'), true)) {
878 $valid_input['image-style'] = 'block';
879 }
880
881 foreach (array('click_to_delete', 'email', 'pdf', 'print') as $opt) {
882 // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
883 if (! isset($input[ $opt ]) || ! in_array($input[ $opt ], array('0', '1'))) {
884 $valid_input[ $opt ] = '0';
885 }
886 }
887 unset($opt);
888
889 if (! isset($input['images-size']) || ! in_array($input['images-size'], array('full-size', 'remove-images', 'large', 'medium', 'small'), true)) {
890 $valid_input['images-size'] = 'full-size';
891 }
892
893 if (! isset($input['css_include_via']) || ! in_array($input['css_include_via'], array('file', 'inline_tag'), true)) {
894 $valid_input['css_include_via'] = 'inline_tag';
895 }
896
897 $css = $input['custom_css'];
898
899 // Strip all HTML/script tags before further sanitization.
900 $css = wp_strip_all_tags($css);
901 $valid_input['custom_css'] = sanitize_textarea_field($css);
902
903
904 // if empty CSS is provided in the box
905 // check if this version is upgraded from the version that was using url instead of textbox
906 // and reuse it
907 if (empty($valid_input['custom_css']) && isset($this->options['custom_css_url'])) {
908 if (empty($this->getVal('custom_css_url'))) {
909 unset($valid_input['custom_css_url']);
910 } else {
911 $valid_input['custom_css_url'] = $this->getVal('custom_css_url');
912 }
913 } else {
914 if (empty($valid_input['custom_css'])) {
915 unset($valid_input['custom_css_url_pro']);
916 } else {
917 if ($valid_input['css_include_via'] === 'file') {
918 $file = $this->generate_custom_css_file($valid_input['custom_css']);
919 $valid_input['custom_css_url_pro'] = $file;
920 }
921 }
922 }
923
924 /* Section 5 options */
925 if (! isset($input['password_protected']) || ! in_array($input['password_protected'], array('no', 'yes'), true)) {
926 $valid_input['password_protected'] = 'no';
927 }
928 if (! isset($input['show_hidden_content']) || ! in_array($input['show_hidden_content'], array('no', 'yes'), true)) {
929 $valid_input['show_hidden_content'] = 'no';
930 }
931
932 /*Analytics Options */
933 if (! isset($input['enable_google_analytics']) || ! in_array($input['enable_google_analytics'], array('no', 'yes'), true)) {
934 $valid_input['enable_google_analytics'] = 'no';
935 }
936
937 if (! isset($input['pf_algo']) || ! in_array($input['pf_algo'], array('wp', 'pf', 'css'), true)) {
938 $valid_input['pf_algo'] = 'wp';
939 }
940
941 if ($valid_input['pf_algo'] === 'css' && empty($valid_input['pf_algo_css_content']) && ! empty($valid_input['css-content'])) {
942 $valid_input['pf_algo_css_content'] = 'original';
943 }
944
945 if (! isset($valid_input['custom_image_width'])) {
946 $valid_input['custom_image_width'] = 0;
947 }
948
949 if (! isset($valid_input['custom_image_height'])) {
950 $valid_input['custom_image_height'] = 0;
951 }
952
953 if (empty($valid_input['custom_image'])) {
954 unset($valid_input['custom_image_width']);
955 unset($valid_input['custom_image_height']);
956 }
957
958 /* Database version */
959 $valid_input['db_version'] = $this->db_version;
960
961 // set the current tab from where the settings were saved from
962 if (isset($_POST['tab'])) {
963 // Nonce already verified via check_admin_referer() at the top of this method.
964 set_transient('pf-tab', sanitize_text_field(wp_unslash($_POST['tab'])), 5);
965 }
966
967 // save the categories as comma-separated
968 if (isset($valid_input['show_on_cat']) && ! empty($valid_input['show_on_cat'])) {
969 $valid_input['show_on_cat'] = implode(',', $valid_input['show_on_cat']);
970 }
971
972 return $valid_input;
973 }
974
975 /**
976 * Register the config page for all users that have the manage_options capability
977 *
978 * @since 3.0
979 */
980 function add_config_page()
981 {
982 $this->settings_page = add_options_page(__('PrintFriendly Options', 'printfriendly'), __('Print Friendly & PDF', 'printfriendly'), 'manage_options', $this->hook, array(&$this, 'config_page'));
983
984 // register callback gets call prior your own page gets rendered
985 add_action('load-' . $this->settings_page, array(&$this, 'on_load_printfriendly'));
986 }
987
988 /**
989 * Enqueue the scripts for the admin settings page
990 *
991 * @since 3.0
992 * @param string $screen_id check whether the current page is the PrintFriendly settings page.
993 */
994 function admin_enqueue_scripts($screen_id)
995 {
996 if ($this->settings_page === $screen_id) {
997 if (! did_action('wp_enqueue_media')) {
998 wp_enqueue_media();
999 }
1000
1001 if (! wp_script_is('clipboard')) {
1002 // Use the clipboard library bundled with WordPress core (registered since 4.9).
1003 wp_enqueue_script('clipboard');
1004 }
1005
1006 // 'registered', not the default 'enqueued': wp_enqueue_script()
1007 // with a $src is a no-op on the registration when the handle
1008 // already exists, so asking the enqueued question meant that a
1009 // plugin which had registered select2 without enqueuing it left
1010 // us passing a version and deps that WordPress silently dropped.
1011 // Skipping outright is honest, and pf-admin's own select2
1012 // dependency pulls whichever copy is registered.
1013 if (! wp_script_is('select2', 'registered')) {
1014 // Versioned with the plugin, like every other asset we ship.
1015 // Without it WordPress falls back to appending its own version,
1016 // so a new select2 build would sit behind browser caches until
1017 // WordPress itself updated. jQuery is an explicit dependency
1018 // because the bundled file calls jQuery at load time.
1019 //
1020 // Still enqueued rather than only registered. Both branches
1021 // below do enqueue something that depends on select2, so
1022 // registering would work today, but this function has already
1023 // been bitten once by exactly that reasoning: see the comment
1024 // on wp_enqueue_script('pf-admin'), added after the v2 branch
1025 // left pf-admin registered with nothing pulling it in and the
1026 // settings page lost its tabs, colour picker and select2.
1027 wp_enqueue_script(
1028 'select2',
1029 plugins_url('assets/js/lib/select2.min.js', __FILE__),
1030 array('jquery'),
1031 $this->plugin_version,
1032 false
1033 );
1034 }
1035
1036 // $in_footer stated rather than left to the default. It is false,
1037 // not true: these run on the settings screen alongside header
1038 // output that expects them, so this records today's behaviour
1039 // instead of quietly moving it.
1040 wp_register_script('pf-admin', plugins_url('assets/js/admin.js', __FILE__), array( 'jquery', 'jquery-ui-tabs', 'jquery-ui-accordion', 'media-upload', 'wp-color-picker', 'clipboard', 'select2' ), $this->plugin_version, false);
1041 wp_localize_script(
1042 'pf-admin',
1043 'pf_config',
1044 array(
1045 'i10n' => array(
1046 'upload_window_title' => __('Custom Image', 'printfriendly'),
1047 'upload_window_button_title' => __('Use Image', 'printfriendly'),
1048 'invalid_image_url' => __('Invalid Image URL', 'printfriendly'),
1049 ),
1050 )
1051 );
1052
1053 if ($this->pro_card) {
1054 // v2 card: server-side status, no browser-side trial script.
1055 // pf-admin is only registered above, and in the legacy branch it
1056 // rides in as a dependency of pf-admin-pro. Nothing pulls it in
1057 // here, so enqueue it directly or the settings page loses its
1058 // tabs, accordions, colour picker, select2 and clipboard.
1059 wp_enqueue_script('pf-admin');
1060 $this->pro_card->enqueue_assets();
1061 } else {
1062 wp_register_script('pf-admin-pro', plugins_url('assets/js/admin_pro.js', __FILE__), array( 'pf-admin' ), $this->plugin_version, false);
1063
1064 wp_localize_script(
1065 'pf-admin-pro',
1066 'pf_config',
1067 array(
1068 'nonce' => wp_create_nonce($this->hook . $this->plugin_version),
1069 'action' => $this->hook,
1070 'i10n' => array(
1071 'activation' => __('Activation', 'printfriendly'),
1072 'check_status' => __('Checking status', 'printfriendly'),
1073 'activate' => __('Activate', 'printfriendly'),
1074 'active_trial' => __('Active Trial', 'printfriendly'),
1075 'active' => __('Active', 'printfriendly'),
1076 'expired' => __('Expired', 'printfriendly'),
1077 'connection' => __('Please check Internet connection.', 'printfriendly'),
1078 ),
1079 )
1080 );
1081 wp_enqueue_script('pf-admin-pro');
1082 }
1083
1084 wp_enqueue_style('pf-bulma', plugins_url('assets/css/lib/bulma.prefixed.min.css', __FILE__), array( 'wp-color-picker' ), $this->plugin_version);
1085 wp_enqueue_style('pf-select2', plugins_url('assets/css/lib/select2.min.css', __FILE__), array(), $this->plugin_version);
1086 wp_enqueue_style('pf-admin', plugins_url('assets/css/admin.css', __FILE__), array( 'pf-select2', 'pf-bulma' ), $this->plugin_version);
1087 }
1088 }
1089
1090 /**
1091 * Register the settings link for the plugins page
1092 *
1093 * @since 3.0
1094 * @param array $links the links for the plugins.
1095 * @param string $file filename to check against plugins filename.
1096 * @return array $links the links with the settings link added to it if appropriate.
1097 */
1098 function filter_plugin_actions($links, $file)
1099 {
1100 // Static so we don't call plugin_basename on every plugin row.
1101 static $this_plugin;
1102 if (! $this_plugin) {
1103 $this_plugin = plugin_basename(__FILE__);
1104 }
1105
1106 if ($file === $this_plugin) {
1107 $settings_link = '<a href="options-general.php?page=' . $this->hook . '">' . __('Settings', 'printfriendly') . '</a>';
1108 array_unshift($links, $settings_link); // before other links
1109 }
1110
1111 return $links;
1112 }
1113
1114 /**
1115 * Register the additional link for the plugins page in the plugin description column.
1116 *
1117 * @since ?
1118 * @param array $links the links for the plugins.
1119 * @param string $file filename to check against plugins filename.
1120 * @return array $links the links with the links added to it if appropriate.
1121 */
1122 function additional_links($links, $file)
1123 {
1124 // Static so we don't call plugin_basename on every plugin row.
1125 static $this_plugin;
1126 if (! $this_plugin) {
1127 $this_plugin = plugin_basename(__FILE__);
1128 }
1129
1130 if ($file === $this_plugin) {
1131 $new_links = array( '<a href="https://support.printfriendly.com/wordpress/customize-content-selection/" target="_new">' . __('Documentation', 'printfriendly') . '</a>' );
1132 $links = array_merge($links, $new_links);
1133 }
1134 return $links;
1135 }
1136
1137 /**
1138 * Set default values for the plugin. If old, as in pre 1.0, settings are there, use them and then delete them.
1139 *
1140 * @since 3.0
1141 */
1142 function set_defaults()
1143 {
1144 // Set some defaults
1145 $this->options = array(
1146 'button_type' => 'buttons/printfriendly-pdf-button.png',
1147 'content_position' => 'left',
1148 'button_alignment_method' => 'default',
1149 'content_position_css' => '',
1150 'content_placement' => 'after',
1151 'custom_button_icon' => 'https://cdn.printfriendly.com/icons/printfriendly-icon-md.png',
1152 'custom_button_text' => 'custom-text',
1153 'custom_image' => '',
1154 'custom_text' => 'PrintFriendly',
1155 'enable_css' => 'on',
1156 'margin_top' => '12',
1157 'margin_right' => '12',
1158 'margin_bottom' => '12',
1159 'margin_left' => '12',
1160 'show_on_posts' => 'on',
1161 'show_on_pages' => 'on',
1162 'text_color' => '#3AAA11',
1163 'text_size' => 14,
1164 'logo' => 'favicon',
1165 'image_url' => '',
1166 'tagline' => '',
1167 'click_to_delete' => '0', // 0 - allow, 1 - do not allow
1168 'hide-images' => '0', // 0 - show images, 1 - hide images
1169 'image-style' => 'block', // 'right', 'left', 'none', 'block'
1170 'email' => '0', // 0 - allow, 1 - do not allow
1171 'pdf' => '0', // 0 - allow, 1 - do not allow
1172 'print' => '0', // 0 - allow, 1 - do not allow
1173 'password_protected' => 'no',
1174 'enable_google_analytics' => 'no',
1175 'enable_error_reporting' => 'yes',
1176 'pf_algo' => 'wp',
1177 'pf_algo_css_content' => 'original',
1178 'images-size' => 'full-size',
1179 'show_hidden_content' => 'no',
1180 'css_include_via' => 'inline_tag',
1181 );
1182
1183 // Check whether the old badly named singular options are there, if so, use the data and delete them.
1184 foreach (array_keys($this->options) as $opt) {
1185 $old_opt = get_option('pf_' . $opt);
1186 if ($old_opt !== false) {
1187 $this->options[ $opt ] = $old_opt;
1188 delete_option('pf_' . $opt);
1189 }
1190 }
1191
1192 // This should always be set to the latest immediately when defaults are pushed in.
1193 $this->options['db_version'] = $this->db_version;
1194
1195 update_option($this->option_name, $this->options);
1196 }
1197
1198 /**
1199 * Upgrades the stored options, used to add new defaults if needed etc.
1200 *
1201 * @since 3.0
1202 */
1203 function upgrade()
1204 {
1205 // update options to version 2
1206 if ($this->options['db_version'] < 2) {
1207 $additional_options = array(
1208 'enable_css' => 'on',
1209 'logo' => 'favicon',
1210 'image_url' => '',
1211 'tagline' => '',
1212 'click_to_delete' => '0',
1213 'password_protected' => 'no',
1214 );
1215
1216 // correcting badly named option
1217 if (isset($this->options['disable_css'])) {
1218 $additional_options['enable_css'] = $this->options['disable_css'];
1219 unset($this->options['disable_css']);
1220 }
1221
1222 // check whether image we do not list any more was used
1223 if (in_array($this->options['button_type'], array('button-print-whgn20.png', 'pf_button_sq_qry_m.png', 'pf_button_sq_qry_l.png', 'pf_button_sq_grn_m.png', 'pf_button_sq_grn_l.png'), true)) {
1224 // previous version had a bug with button name
1225 if (in_array($this->options['button_type'], array('pf_button_sq_qry_m.png', 'pf_button_sq_qry_l.png'), true)) {
1226 $this->options['button_type'] = str_replace('qry', 'gry', $this->options['button_type']);
1227 }
1228
1229 $image_address = 'https://cdn.printfriendly.com/' . $this->options['button_type'];
1230 $this->options['button_type'] = 'custom-image';
1231 $this->options['custom_text'] = '';
1232 $this->options['custom_image'] = $image_address;
1233 }
1234
1235 $this->options = array_merge($this->options, $additional_options);
1236 }
1237
1238 // update options to version 3
1239 if ($this->options['db_version'] < 3) {
1240 $old_show_on = $this->options['show_list'];
1241 // 'manual' setting
1242 $additional_options = array('custom_css_url' => '');
1243
1244 if ($old_show_on === 'all') {
1245 $additional_options = array(
1246 'show_on_pages' => 'on',
1247 'show_on_posts' => 'on',
1248 'show_on_homepage' => 'on',
1249 'show_on_categories' => 'on',
1250 'show_on_taxonomies' => 'on',
1251 );
1252 }
1253
1254 if ($old_show_on === 'single') {
1255 $additional_options = array(
1256 'show_on_pages' => 'on',
1257 'show_on_posts' => 'on',
1258 );
1259 }
1260
1261 if ($old_show_on === 'posts') {
1262 $additional_options = array(
1263 'show_on_posts' => 'on',
1264 );
1265 }
1266
1267 unset($this->options['show_list']);
1268
1269 $this->options = array_merge($this->options, $additional_options);
1270 }
1271
1272 // update options to version 4
1273 if ($this->options['db_version'] < 4) {
1274 $additional_options = array(
1275 'email' => '0',
1276 'pdf' => '0',
1277 'print' => '0',
1278 );
1279
1280 $this->options = array_merge($this->options, $additional_options);
1281 }
1282
1283 // update options to version 6
1284 // Replacement for db version 5 - should also be run for those already upgraded
1285 if ($this->options['db_version'] < 6) {
1286 unset($this->options['category_ids']);
1287 }
1288
1289 if ($this->options['db_version'] < 7) {
1290 $additional_options = array(
1291 'hide-images' => '0',
1292 'image-style' => 'right',
1293 );
1294
1295 $this->options = array_merge($this->options, $additional_options);
1296 }
1297
1298 if ($this->options['db_version'] < 8) {
1299 $this->options['enable_google_analytics'] = 'no';
1300 }
1301
1302 if ($this->options['db_version'] < 9) {
1303 $this->options['pf_algo'] = 'wp';
1304 }
1305
1306 if ($this->options['db_version'] < 10) {
1307 $this->options['enable_error_reporting'] = 'yes';
1308 }
1309
1310 if ($this->options['db_version'] < 11) {
1311 if (! isset($this->options['custom_css_url'])) {
1312 $this->options['custom_css_url'] = '';
1313 }
1314 }
1315
1316 if ($this->options['db_version'] < 12) {
1317 // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
1318 $this->options['images-size'] = $this->options['hide-images'] == '1' ? 'remove-images' : 'full-size';
1319 }
1320
1321 if ($this->options['db_version'] < 13) {
1322 switch ($this->options['button_type']) {
1323 case 'pf-button.gif':
1324 $this->options['button_type'] = 'buttons/printfriendly-button.png';
1325 break;
1326 case 'pf-button-both.gif':
1327 $this->options['button_type'] = 'buttons/printfriendly-pdf-button.png';
1328 break;
1329 case 'pf-button-big.gif':
1330 $this->options['button_type'] = 'buttons/printfriendly-button-lg.png';
1331 break;
1332 case 'pf-button-print-pdf-mail.png':
1333 $this->options['button_type'] = 'buttons/printfriendly-pdf-email-button-notext.png';
1334 break;
1335 case 'button-print-grnw20.png':
1336 $this->options['button_type'] = 'buttons/print-button.png';
1337 break;
1338 case 'button-print-blu20.png':
1339 $this->options['button_type'] = 'buttons/print-button-nobg.png';
1340 break;
1341 case 'button-print-gry20.png':
1342 $this->options['button_type'] = 'buttons/print-button-gray.png';
1343 break;
1344 case 'pf-icon-small.gif':
1345 case 'pf-icon.gif':
1346 $this->options['button_type'] = 'custom-button';
1347 $this->options['custom_button_icon'] = 'icons/printfriendly-icon-md.png';
1348 $this->options['custom_button_text'] = 'custom-text';
1349 $this->options['custom_image'] = 'icons/printfriendly-icon-md.png';
1350 break;
1351 case 'text-only':
1352 $this->options['button_type'] = 'custom-button';
1353 $this->options['custom_button_icon'] = 'no-image';
1354 $this->options['custom_button_text'] = 'custom-text';
1355 break;
1356 }
1357 }
1358
1359 if ($this->options['db_version'] < 14) {
1360 if ($this->options['button_type'] === 'pf-icon-both.gif') {
1361 $this->options['button_type'] = 'buttons/printfriendly-pdf-button-nobg.png';
1362 }
1363 }
1364
1365 if ($this->options['db_version'] < 15) {
1366 if ($this->options['button_type'] === 'custom-image') {
1367 $this->options['button_type'] = 'custom-button';
1368
1369 switch ($this->options['custom_image']) {
1370 case 'icons/printfriendly-icon-sm.png':
1371 case 'icons/printfriendly-icon-md.png':
1372 case 'icons/printfriendly-icon-lg.png':
1373 $this->options['custom_button_icon'] = $this->options['custom_image'];
1374 break;
1375 case '':
1376 $this->options['custom_button_icon'] = 'no-image';
1377 break;
1378 default:
1379 $this->options['custom_button_icon'] = 'custom-image';
1380 }
1381
1382 if ($this->options['custom_text'] === '') {
1383 $this->options['custom_button_text'] = 'no-text';
1384 } else {
1385 $this->options['custom_button_text'] = 'custom-text';
1386 }
1387 }
1388 }
1389
1390 if ($this->options['db_version'] < 16) {
1391 if ($this->options['custom_button_icon'] === 'icons/printfriendly-icon-md.png') {
1392 $this->options['custom_button_icon'] = 'https://cdn.printfriendly.com/icons/printfriendly-icon-md.png';
1393 }
1394 }
1395
1396 if ($this->options['db_version'] < 17) {
1397 $this->options['pro_email'] = get_bloginfo('admin_email');
1398
1399 $url = get_bloginfo('url');
1400 $parsed_url = wp_parse_url($url);
1401 $this->options['pro_domain'] = $parsed_url['host'];
1402 }
1403
1404 if ($this->options['db_version'] < 21) {
1405 if (empty($this->getVal('custom_css'))) {
1406 $this->options['css_include_via'] = 'inline_tag';
1407 } else {
1408 $this->options['css_include_via'] = 'file';
1409 }
1410 }
1411
1412 if ($this->options['db_version'] < 22) {
1413 // Backfill options that set_defaults() did not know about. Installs that
1414 // never saved the settings form were missing both keys, which produced
1415 // "Undefined array key" warnings on the settings page.
1416 if (! isset($this->options['pf_algo_css_content'])) {
1417 $this->options['pf_algo_css_content'] = 'original';
1418 }
1419 if (! isset($this->options['content_position_css'])) {
1420 $this->options['content_position_css'] = '';
1421 }
1422 }
1423
1424 $this->options['db_version'] = $this->db_version;
1425
1426 update_option($this->option_name, $this->options);
1427 }
1428
1429 /**
1430 * Displays radio button in the admin area
1431 *
1432 * @since 3.0
1433 * @param string $name the name of the radio button to generate.
1434 * @param boolean $br whether or not to add an HTML <br> tag, defaults to true.
1435 * @param boolean $value if this is null, will have the same value as $name.
1436 */
1437 function radio($name, $br = false, $value = null)
1438 {
1439 if (is_null($value)) {
1440 $value = $name;
1441 }
1442
1443 $var = '<input id="' . esc_attr($name) . '" class="radio" name="' . esc_attr($this->option_name) . '[button_type]" type="radio" value="' . esc_attr($value) . '" ' . $this->checked('button_type', $value, false) . '/>';
1444 $button = $this->button($name);
1445 if (! empty($button)) {
1446 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $name escaped via esc_attr(); $var and $button are pre-escaped HTML fragments.
1447 echo '<label for="' . esc_attr($name) . '">' . $var . $button . '</label>';
1448 } else {
1449 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Pre-escaped HTML fragment assembled above.
1450 echo $var;
1451 }
1452
1453 if ($br) {
1454 echo '<br>';
1455 }
1456 }
1457
1458 /**
1459 * Displays radio button in the admin area
1460 *
1461 * @since 3.12.0
1462 * @param string $value the value of the radio button to generate.
1463 */
1464 function radio_custom_image($value)
1465 {
1466 $button = $this->button($value);
1467 $value = "https://cdn.printfriendly.com/{$value}";
1468 ?>
1469 <label for="<?php echo esc_attr($value); ?>">
1470 <input type="radio" id="<?php echo esc_attr($value); ?>" name="<?php echo esc_attr($this->option_name); ?>[custom_button_icon]" value="<?php echo esc_url($value); ?>" <?php $this->checked('custom_button_icon', $value); ?>>
1471 <?php
1472 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Button markup built with esc_url()/esc_html() inside button().
1473 echo $button;
1474 ?>
1475 </label>
1476 <?php
1477 }
1478
1479 /**
1480 * Displays button image in the admin area
1481 *
1482 * @since 3.0
1483 * @param string $name the name of the button to generate.
1484 */
1485 function button($name = false)
1486 {
1487 if (! $name) {
1488 $name = $this->getVal('button_type');
1489 }
1490
1491 $img_path = 'https://cdn.printfriendly.com/';
1492
1493 $return = '';
1494 $imgStyle = '';
1495
1496 if ($name === 'custom-button') {
1497 if ($this->getVal('custom_button_icon') === 'custom-image' && '' !== trim($this->getVal('custom_image', ''))) {
1498 $width = $this->getVal('custom_image_width');
1499 $height = $this->getVal('custom_image_height');
1500 if (! empty($width)) {
1501 $imgStyle .= sprintf('width: %dpx;', $width);
1502 }
1503 if (! empty($height)) {
1504 $imgStyle .= sprintf('height: %dpx;', $height);
1505 }
1506
1507 $return = '<img src="' . esc_url($this->getVal('custom_image')) . '" alt="Print Friendly, PDF & Email" class="pf-button-img" style="' . $imgStyle . '" />';
1508 } elseif ($this->getVal('custom_button_icon') !== 'no-image') {
1509 $icon = str_replace($img_path, '', $this->getVal('custom_button_icon'));
1510 $attributes = self::$_buttons[ $icon ];
1511 $atts = '';
1512 $width = $attributes['width'];
1513 $height = $attributes['height'];
1514 if (! empty($width)) {
1515 $imgStyle .= sprintf('width: %dpx;', $width);
1516 }
1517 if (! empty($height)) {
1518 $imgStyle .= sprintf('height: %dpx;', $height);
1519 }
1520
1521 $return = '<img src="' . esc_url($this->getVal('custom_button_icon')) . '" alt="Print Friendly, PDF & Email" class="pf-button-img" style="' . $imgStyle . '" />';
1522 }
1523
1524 if ($this->getVal('custom_button_text') === 'custom-text') {
1525 $return .= sprintf('<span id="printfriendly-text2" class="pf-button-text">%s</span>', $this->esc_html_if_needed($this->getVal('custom_text')));
1526 }
1527
1528 return $return;
1529 } elseif ($name === 'custom-btn') {
1530 return __('Custom Button', 'printfriendly');
1531 } else {
1532 $attributes = self::$_buttons[ $name ];
1533 $atts = '';
1534 $width = $attributes['width'];
1535 $height = $attributes['height'];
1536 if (! empty($width)) {
1537 $imgStyle .= sprintf('width: %dpx;', $width);
1538 }
1539 if (! empty($height)) {
1540 $imgStyle .= sprintf('height: %dpx;', $height);
1541 }
1542
1543 return '<img class="pf-button-img" src="' . $img_path . $name . '" alt="Print Friendly, PDF & Email" style="' . $imgStyle . '" />';
1544 }
1545 }
1546
1547 /**
1548 * Convenience function to output a value custom button preview elements
1549 *
1550 * @since 3.0.9
1551 */
1552 function custom_button_preview()
1553 {
1554 $img = $url = $button_text = $style = $width = $height = '';
1555 switch ($this->getVal('custom_button_icon')) {
1556 case 'no-image':
1557 break;
1558 case 'custom-image':
1559 $url = $this->getVal('custom_image');
1560 $width = $this->getVal('custom_image_width');
1561 $height = $this->getVal('custom_image_height');
1562 break;
1563 default:
1564 $url = $this->getVal('custom_button_icon');
1565 break;
1566 }
1567
1568 if (! empty($url)) {
1569 $imgStyle = '';
1570 if (! empty($width)) {
1571 $imgStyle .= sprintf('width: %dpx;', $width);
1572 }
1573 if (! empty($height)) {
1574 $imgStyle .= sprintf('height: %dpx;', $height);
1575 }
1576 $img = sprintf('<img src="%s" style="%s" alt="Print Friendly, PDF & Email">', esc_url($url), $imgStyle);
1577 }
1578
1579 if ($this->getVal('custom_button_text') !== 'no-text') {
1580 $button_text = $this->getVal('custom_text');
1581 }
1582
1583 if ('' !== $this->getVal('text_color', '')) {
1584 $style .= 'color: ' . $this->getVal('text_color') . ';';
1585 }
1586
1587 $button_preview = sprintf('<span><span id="pf-custom-button-preview" class="pf-button-img">%s</span><span id="printfriendly-text2" class="pf-button-text" style="%s">%s</span></span>', $img, $style, $this->esc_html_if_needed($button_text));
1588
1589 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Assembled from esc_url()/esc_html_if_needed() parts and a hex-validated text_color.
1590 echo $button_preview;
1591 }
1592
1593 function echoVal($option, $default = '')
1594 {
1595 // Always escape for the HTML attribute context this helper is used in.
1596 echo esc_attr($this->getVal($option, $default));
1597 }
1598
1599 function getVal($option, $default = null)
1600 {
1601 $value = $default;
1602 if (isset($this->options[ $option ])) {
1603 $value = $this->options[ $option ];
1604 }
1605
1606 return $value;
1607 }
1608
1609
1610 /**
1611 * Like the WordPress checked() function but it doesn't throw notices when the array key isn't set and uses the plugins options array.
1612 *
1613 * @since 3.0
1614 * @param mixed $val value to check.
1615 * @param mixed $check_against value to check against.
1616 * @param boolean $echo whether or not to echo the output.
1617 * @return string checked, when true, empty, when false.
1618 */
1619 function checked($val, $check_against = true, $echo = true)
1620 {
1621 $result = '';
1622 if (! isset($this->options[ $val ])) {
1623 return $result;
1624 }
1625
1626 // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
1627 if ($this->options[ $val ] == $check_against) {
1628 $result = ' checked="checked" ';
1629 if ($echo) {
1630 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Static ' checked="checked" ' attribute string.
1631 echo $result;
1632 }
1633 }
1634 return $result;
1635 }
1636
1637 /**
1638 * Helper for creating checkboxes.
1639 *
1640 * @since 3.1.5
1641 * @param string $name string used for various parts of checkbox.
1642 */
1643 function create_checkbox($name, $label = '', $labelid = '')
1644 {
1645 // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText
1646 $label = ( ! empty($label) ? $label : __(ucfirst($name), 'printfriendly') );
1647 echo '<label' . ( ! empty($labelid) ? ' id="' . esc_attr($labelid) . '"' : '' ) . '><input type="checkbox" class="show_list" name="' . esc_attr($this->option_name) . '[show_on_' . esc_attr($name) . ']" value="on" ';
1648 $this->checked('show_on_' . $name, 'on');
1649 echo ' />' . esc_html($label) . "</label>\r\n";
1650 }
1651
1652
1653 /**
1654 * Like the WordPress selected() function but it doesn't throw notices when the array key isn't set and uses the plugins options array.
1655 *
1656 * @since 3.0.9
1657 * @param mixed $val value to check.
1658 * @param mixed $check_against value to check against.
1659 * @return string checked, when true, empty, when false.
1660 */
1661 function selected($val, $check_against = true)
1662 {
1663 if (! isset($this->options[ $val ])) {
1664 return;
1665 }
1666
1667 return selected($this->options[ $val ], $check_against);
1668 }
1669
1670 /**
1671 * For use with page metabox.
1672 *
1673 * @since 3.2.2
1674 */
1675 function get_page_post_type()
1676 {
1677 $post_types = get_post_types(array('name' => 'page'), 'object');
1678 // echo '<pre>'.print_r($post_types,1).'</pre>';
1679 // die;
1680
1681 return $post_types['page'];
1682 }
1683
1684
1685 /**
1686 * Helper that checks if wp versions is above 3.0.
1687 *
1688 * @since 3.2.2
1689 * @return boolean true wp version is above 3.0
1690 */
1691 function wp_version_gt30()
1692 {
1693 global $wp_version;
1694 return version_compare($wp_version, '3.0', '>=');
1695 }
1696
1697
1698 /**
1699 * Create box for picking individual categories.
1700 *
1701 * @since 3.2.2
1702 */
1703 function create_category_metabox()
1704 {
1705 $obj = new stdClass();
1706 $obj->ID = 0;
1707 do_meta_boxes('settings_page_' . $this->hook, 'normal', $obj);
1708 }
1709
1710
1711 /**
1712 * Load metaboxes advanced button display settings.
1713 *
1714 * @since 3.2.2
1715 */
1716 function on_load_printfriendly()
1717 {
1718 global $wp_version;
1719 if ($this->wp_version_gt30()) {
1720 // require_once(dirname(__FILE__).'/includes/meta-boxes.php');
1721 // require_once(dirname(__FILE__).''includes/nav-menu.php');
1722 wp_enqueue_script('post');
1723
1724 add_meta_box('categorydiv', __('Only display when post is in:', 'printfriendly'), 'post_categories_meta_box', 'settings_page_' . $this->hook, 'normal', 'core');
1725 }
1726 }
1727
1728 /**
1729 * Previously, users with capability unfiltered_html were allowed to use raw HTML.
1730 * But now, the input for all users is sanitized.
1731 */
1732 function esc_html_if_needed($input)
1733 {
1734 $final = esc_html(wp_strip_all_tags($input));
1735 return $final;
1736 }
1737
1738 /**
1739 * Returns if the user is a pro user.
1740 */
1741 function is_pro($feature = null)
1742 {
1743 $licensed = true;//$this->getVal('license_status') === 'pro';
1744
1745 switch ($feature) {
1746 case 'custom-css':
1747 // custom css needs to be available for all irrespective of the license.
1748 return true;
1749 }
1750
1751 return $licensed;
1752 }
1753
1754 function get_custom_css_tag()
1755 {
1756 $custom_css = $this->getVal('custom_css');
1757 if (!empty($custom_css) && 'inline_tag' === $this->getVal('css_include_via')) {
1758 return sprintf('<printfriendly-css style="display: none;">%s</printfriendly-css>', esc_html($custom_css));
1759 }
1760 }
1761
1762 function get_custom_css_js_var()
1763 {
1764 $css_url = $this->get_custom_css_url();
1765 if (!empty($css_url)) {
1766 return sprintf('var pfCustomCSS = "%s";', esc_js(esc_url($css_url)));
1767 }
1768 }
1769
1770 function get_custom_css_url()
1771 {
1772 $css_url = $this->getVal('custom_css_url', '');
1773
1774 if (empty($css_url) && 'file' === $this->getVal('css_include_via')) {
1775 // It could be unset because custom css is empty
1776 $css_url = $this->getVal('custom_css_url_pro');
1777 if (!empty($css_url)) {
1778 $dirs = wp_get_upload_dir();
1779 $css_url = $dirs['baseurl'] . '/' . $css_url;
1780 }
1781 }
1782 return $css_url;
1783 }
1784
1785
1786 /**
1787 * Generates the custom css file from the CSS block.
1788 */
1789 function generate_custom_css_file($css)
1790 {
1791 $file = $this->getVal('custom_css_url_pro');
1792
1793 $custom_css_old = $this->getVal('custom_css');
1794
1795 // return the old file if the CSS has not changed.
1796 if ($custom_css_old === $css && ! empty($file)) {
1797 return $file;
1798 }
1799
1800 $dirs = wp_get_upload_dir();
1801
1802 // delete old file, if it exists
1803 if (! empty($file)) {
1804 wp_delete_file($dirs['basedir'] . '/' . $file);
1805 }
1806
1807 // add a comment so that users know whence this file came.
1808 $date = date_i18n(get_option('date_format') . ' ' . get_option('time_format'));
1809 $comment = sprintf('/* DO NOT EDIT - FILE AUTO-GENERATED BY PRINTFRIENDLY v%s ON %s */', $this->plugin_version, $date);
1810 $css = $comment . PHP_EOL . PHP_EOL . $css;
1811
1812 // create new file, suffixed with the current time.
1813 $file = sprintf('%s_%s.css', $this->hook, time());
1814 include_once ABSPATH . 'wp-admin/includes/file.php';
1815 WP_Filesystem();
1816 global $wp_filesystem;
1817 $wp_filesystem->put_contents(
1818 $dirs['basedir'] . '/' . $file,
1819 $css,
1820 FS_CHMOD_FILE
1821 );
1822
1823 // return the new file name
1824 return $file;
1825 }
1826
1827 /**
1828 * If upgrading from a previous version that was using urls instead of the textarea
1829 * it will return an appropriate message for the user.
1830 *
1831 * Returns an empty string when no legacy custom_css_url option is set, so the
1832 * result can be passed straight to wp_kses_post() without a null-argument
1833 * deprecation on PHP 8.1+.
1834 *
1835 * @return string
1836 */
1837 function get_custom_css_upgrade_message()
1838 {
1839 // upgrading from a version that was using urls instead of the textarea?
1840 $css_url = $this->getVal('custom_css_url');
1841 if (! empty($css_url)) {
1842 // translators: %1$s is an opening <a> tag, %2$s is the CSS file URL, %3$s is the closing </a> tag.
1843 return sprintf(__('You are currently using %1$s%2$s%3$s. You can copy its contents into the textbox if you want to update the styles.', 'printfriendly'), '<a href="' . esc_url($css_url) . '" target="_blank">', esc_html($css_url), '</a>');
1844 }
1845
1846 return '';
1847 }
1848
1849 /**
1850 * Output the config page
1851 *
1852 * @since 3.0
1853 */
1854 function config_page()
1855 {
1856
1857 // Since WP 3.2 outputs these errors by default, only display them when we're on versions older than 3.2 that do support the settings errors.
1858 global $wp_version;
1859 if (version_compare($wp_version, '3.2', '<') && $this->wp_version_gt30()) {
1860 settings_errors();
1861 }
1862
1863 $customCssOptionCode = $this->getSelectorsFromCustomCSS();
1864 include_once PRINTFRIENDLY_BASEPATH . '/views/settings.php';
1865 }
1866
1867 /**
1868 * Returns the current tab to activate.
1869 *
1870 * @since 4.0.1
1871 */
1872 function is_tab($tab_id)
1873 {
1874 $tab = get_transient('pf-tab');
1875 // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
1876 return empty($tab) && $tab_id == 0 ? true : $tab == $tab_id;
1877 }
1878 }
1879 $printfriendly = new PrintFriendly_WordPress();
1880 }
1881
1882 // Add shortcode for printfriendly button
1883 add_shortcode(
1884 'printfriendly',
1885 /**
1886 * Show the printfriendly button.
1887 *
1888 * @param array $atts The attributes of the shortcode.
1889 * If current="yes", the whole page will be printed (pf_current_page_button is called).
1890 * The default behaviour is to call pf_default_button.
1891 *
1892 * @since 3.0
1893 *
1894 * @return string The button.
1895 */
1896 function ($atts) {
1897 $atts = shortcode_atts(
1898 array(
1899 'current' => '', // yes => print the whole page
1900 ),
1901 $atts,
1902 'printfriendly'
1903 );
1904
1905 if (empty($atts['current'])) {
1906 return pf_default_button();
1907 } else {
1908 return pf_current_page_button();
1909 }
1910
1911 return null;
1912 }
1913 );
1914
1915
1916 /**
1917 * Convenience function for use in templates.
1918 *
1919 * @deprecated Use pf_default_button instead.
1920 *
1921 * @since 3.0
1922 *
1923 * @return string returns a button to be printed.
1924 */
1925 function pf_show_link()
1926 {
1927 return pf_default_button();
1928 }
1929
1930 /**
1931 * Convenience function for use in templates.
1932 *
1933 * @since 5.1
1934 *
1935 * @return string returns a button to be printed.
1936 */
1937 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Public template tag (since 3.0); renaming would break themes and shortcodes that call it.
1938 function pf_default_button()
1939 {
1940 global $printfriendly;
1941 return $printfriendly->getButton(true);
1942 }
1943
1944 /**
1945 * Convenience function for use in templates.
1946 *
1947 * @since 5.1
1948 *
1949 * @return string returns a button that prints the entire page it is on.
1950 */
1951 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Public template tag (since 5.1); renaming would break themes and shortcodes that call it.
1952 function pf_current_page_button()
1953 {
1954 global $printfriendly;
1955 return $printfriendly->getButton(true, '', true);
1956 }
1957