PluginProbe
Participants Database / trunk
Participants Database vtrunk
2.7.8.5 2.7.8.4 2.7.8.3 2.6.1 2.6.2 2.7 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.5.1 2.7.6 2.7.6.1 2.7.6.2 2.7.6.3 2.7.7 2.7.8 2.7.8.1 2.7.8.2 1.4.5.2 2.5.9.2 1.4.6 2.5.9.3 1.4.7 All 313 releases
participants-database / participants-database.php

participants-database.php in Participants Database trunk, at participants-database.php

3,917 lines 128.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: Participants Database
4 * Plugin URI: https://xnau.com/wordpress-plugins/participants-database
5 * Description: Plugin for managing a database of participants, members or volunteers
6 * Author: Roland Barker, xnau webdesign
7 * Version: 2.7.8.5
8 * Author URI: https://xnau.com
9 * License: GPL3
10 * Text Domain: participants-database
11 * Domain Path: /languages
12 */
13 /*
14 *
15 *
16 *
17 * Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025 Roland Barker xnau webdesign (email : webdesign@xnau.com)
18 *
19 * This program is free software; you can redistribute it and/or modify it
20 * under the terms of the GNU General Public License, version 3, as
21 * published by the Free Software Foundation.
22 *
23 * This program is distributed in the hope that it will be useful, but
24 * WITHOUT ANY WARRANTY; without even the implied warranty of
25 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
26 * General Public License for more details.
27 *
28 * You should have received a copy of the GNU General Public License along
29 * with this program; if not, write to the Free Software Foundation, Inc.,
30 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
31 *
32 */
33 defined( 'ABSPATH' ) || exit;
34
35 // register the class autoloading
36 spl_autoload_register( 'PDb_class_loader' );
37
38 /**
39 * main static class for running the plugin
40 *
41 * @category WordPress Plugins
42 * @package wordPress
43 * @author Roland Barker <webdesign@xnau.com>
44 * @copyright 2011 - 2022 7th Veil, LLC
45 * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL2
46 * @version Release: 2.1
47 *
48 */
49 class Participants_Db extends PDb_Base {
50
51 /**
52 * @var string sets the min PHP version level required
53 */
54 const min_php_version = '7.4';
55
56 /**
57 *
58 * unique slug for the plugin; this is same as the plugin directory name
59 *
60 * @var string unique slug for the plugin
61 */
62 const PLUGIN_NAME = 'participants-database';
63
64 /**
65 * @var string name of the single record query var
66 */
67 public static $single_query;
68
69 /**
70 * @var string name of the record edit query var
71 */
72 public static $record_query;
73
74 /**
75 * display title
76 * @var string
77 */
78 public static $plugin_title;
79
80 /**
81 * basename of the main participants index table
82 * @var string
83 */
84 public static $participants_table;
85
86 /**
87 * base name of the table for all associated values
88 * @var string
89 */
90 public static $fields_table;
91
92 /**
93 * name of the table for groups defninitions
94 * @var string
95 */
96 public static $groups_table;
97
98 /**
99 * to create a new database version, change this value to the new version number.
100 * This will trigger a database update in the PDb_Init class
101 *
102 * @var string current Db version
103 */
104 public static $db_version = '1.3';
105
106 /**
107 * name of the WP option where the current db version is stored
108 * @var string
109 */
110 public static $db_version_option = 'PDb_Db_version';
111
112 /**
113 * current version of plugin
114 * @var string
115 */
116 public static $plugin_version;
117
118 /**
119 * name of the WP plugin options
120 * @var string
121 */
122 public static $participants_db_options = self::PLUGIN_NAME . '_options';
123
124 /**
125 * name of the default settings option
126 * @var string
127 */
128 public static $default_options;
129
130 /**
131 * plugin option values $name => $value
132 * @var array
133 */
134 public static $plugin_options;
135
136 /**
137 * plugin settings object
138 * @var object
139 */
140 public static $Settings;
141
142 /**
143 * name of the plugin admin page
144 * @var string
145 */
146 public static $plugin_page;
147
148 /**
149 * path to the plugin root directory
150 * @var string
151 */
152 public static $plugin_path;
153
154 /**
155 * path to the main plugin file
156 * @var string
157 */
158 public static $plugin_file;
159
160 /**
161 * a general-use prefix to set a namespace
162 *
163 * @var string
164 */
165 public static $prefix = 'pdb-';
166
167 /**
168 * duplicate of $prefix for backwards compatibility
169 * @var string
170 */
171 public static $css_prefix;
172
173 /**
174 * the PDb_FormValidation object
175 * @var PDb_FormValidation
176 */
177 public static $validation_errors;
178
179 /**
180 * name of the transient record used to hold the last recor
181 * @var string
182 */
183 public static $last_record;
184
185 /**
186 * status code for the last record processed
187 * @var string
188 */
189 public static $insert_status;
190
191 /**
192 * header to include with plugin email
193 * @var strings
194 */
195 public static $email_headers;
196
197 /**
198 * list of reserved field names
199 * @var array
200 */
201 public static $reserved_names = array('source', 'subsource', 'id', 'private_id', 'record_link', 'action', 'submit', 'submit-button', 'name', 'day', 'month', 'year', 'hour', 'date', 'minute', 'email-regex', 'combo_search', 'fields', 'groups');
202
203 /**
204 * true while sending an email
205 * @var bool
206 */
207 public static $sending_email = false;
208
209 /**
210 * set of internationalized words
211 * @var array
212 */
213 public static $i18n = array();
214
215 /**
216 * the last method used to parse a date
217 *
218 * @var string
219 */
220 public static $date_mode;
221
222 /**
223 * index for tracking multiple instances of a shortcode
224 * @var int
225 */
226 public static $instance_index = 1;
227
228 /**
229 * @var string name of the list pagination variable
230 */
231 public static $list_page = 'listpage';
232
233 /**
234 * holds the WP session object
235 *
236 * @var PDb_Session
237 */
238 public static $session;
239
240 /**
241 * this is set once per plugin instantiation, then all instances are expected to use this instead of running their own queries
242 *
243 * @var array of PDb_Form_Field_Def objects, indexed by field name
244 */
245 public static $fields = array();
246
247 /**
248 * @var string context string for the main submission nonce
249 */
250 public static $main_submission_nonce_key = 'main_submission';
251
252 /**
253 * @var int the number of characters to use in the private ID
254 */
255 public static $private_id_length = 7;
256
257 /**
258 * @var int maximum number of emails to send per session
259 *
260 * this must be small enough to prevent a script timeout and/or stay under the
261 * typical email rate limit for shared hosting.
262 */
263 public static $mass_email_session_limit = 100;
264
265 /**
266 * @var string option flag for admin notices that are shown only once
267 */
268 const one_time_notice_flag = 'pdb-one-time-notice-shown';
269
270 /**
271 * initializes the static class
272 *
273 * sets up the class autoloading, configuration values, hooks, filters and shortcodes
274 *
275 * @global wpdb $wpdb
276 * @param bool $activate options flag for a non-activation use of this method
277 */
278 public static function initialize( $activate = true )
279 {
280 // set the plugin version
281 self::$plugin_version = self::_get_plugin_data( 'Version' );
282
283 // define some locations
284 self::$default_options = self::$prefix . 'default_options';
285 self::$plugin_page = self::PLUGIN_NAME;
286 self::$plugin_path = plugin_dir_path( __FILE__ );
287 self::$plugin_file = __FILE__;
288
289 // set the debug global if not already
290 self::set_debug_mode();
291
292 // start sessions management
293 self::$session = PDb_Session::get_instance();
294
295 // add the composer autoload
296 require_once self::$plugin_path . '/vendor/autoload.php';
297
298 self::$last_record = self::$prefix . 'last_record';
299 self::$css_prefix = self::$prefix;
300
301 // install/deactivate and uninstall methods are handled by the PDB_Init class
302 register_activation_hook( __FILE__, array('PDb_Init', 'on_activate') );
303 register_deactivation_hook( __FILE__, array('PDb_Init', 'on_deactivate') );
304 register_uninstall_hook( __FILE__, array('PDb_Init', 'on_uninstall') );
305
306 // admin plugin list display
307 add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), array(__CLASS__, 'add_plugin_action_links') );
308 add_filter( 'plugin_row_meta', array(__CLASS__, 'add_plugin_meta_links'), 10, 2 );
309 add_filter( 'all_plugins', array( __CLASS__, 'filter_plugin_data' ) );
310
311 // set the WP hooks to finish setting up the plugin
312 add_action( 'plugins_loaded', array(__CLASS__, 'setup_source_names'), 1 );
313 add_action( 'plugins_loaded', array(__CLASS__, 'init'), 5 );
314 add_action('init', [ __CLASS__,'setup_translations'] );
315 add_action( 'wp', array(__CLASS__, 'check_for_shortcode'), 1 );
316 add_action( 'wp', array(__CLASS__, 'remove_rel_link') );
317
318 add_filter( 'body_class', array(__CLASS__, 'add_body_class') );
319 add_action( 'admin_menu', array(__CLASS__, 'plugin_menu') );
320 add_action( 'admin_init', array(__CLASS__, 'admin_init') );
321 add_action( 'admin_init', array(__CLASS__, 'reg_page_setting_fix') );
322 add_action( 'wp_enqueue_scripts', array(__CLASS__, 'register_assets'), 1 );
323
324 add_action( 'wp_loaded', array(__CLASS__, 'process_page_request') ); // wp_loaded
325 //
326 add_action( 'admin_enqueue_scripts', array(__CLASS__, 'admin_includes') );
327
328 // this is only fired if there is a plugin shortcode on the page
329 add_action( 'pdb-shortcode_present', array(__CLASS__, 'add_shortcode_includes') );
330
331 // clear page caches on the pdb-clear_page_cache action
332 add_action( 'pdb-clear_page_cache', array( 'PDb_Base', 'flush_page_cache' ) );
333
334 /**
335 * MULTISITE
336 *
337 * this is so the Participants Database table names are in sync with the current network blog
338 */
339 add_action( 'switch_blog', array(__CLASS__, 'setup_source_names' ) );
340 // set up the database for any new blogs
341 add_action( 'wpmu_new_blog', array( 'PDb_Init', 'new_blog' ) );
342 add_action( 'delete_blog', array( 'PDb_Init', 'delete_blog' ), 10, 2 );
343
344 /**
345 * @since 1.6.3
346 * added global constant to enable multilingual content of the type that qtranslate-x
347 * employs, where all translations are in the same content and get filtered by
348 * the chosen locale value
349 *
350 * we needed to do this because the callback is somewhat expensive and most installs
351 * are not multilingual
352 *
353 * the PDB_MULTILINGUAL constant must be set to 1 or true to enable the multilingual
354 * filter
355 *
356 * the pdb-translate_string filter can be used in other ways: all display strings
357 * are passed through it
358 */
359 if ( defined( 'PDB_MULTILINGUAL' ) && (bool) PDB_MULTILINGUAL === true ) {
360 add_filter( 'pdb-translate_string', array(__CLASS__, 'string_static_translation'), 20 );
361 }
362
363 // handles ajax request from list filter
364 add_action( 'wp_ajax_pdb_list_filter', array(__CLASS__, 'pdb_list_filter') );
365 add_action( 'wp_ajax_nopriv_pdb_list_filter', array(__CLASS__, 'pdb_list_filter') );
366
367 // define our shortcodes
368 foreach( self::plugin_shortcode_list() as $tag ) {
369 add_shortcode( $tag, array(__CLASS__, 'print_shortcode') );
370 }
371
372 // #2755
373 //add_filter( 'pre_do_shortcode_tag', array( __CLASS__, 'fix_shortcode_special_chars' ), 10, 3);
374 add_filter('the_content', 'Participants_Db::fix_shortcode_special_chars_content', 10 );
375
376 // action for handling the list columns UI
377 add_action( 'wp_ajax_' . PDb_Manage_List_Columns::action, 'PDb_Manage_List_Columns::process_request' );
378
379 // register some plugin events
380 add_filter( 'pdb-register_global_event', [ __CLASS__,'register_events']);
381
382 if ( ! function_exists( 'deactivate_plugins' ) ) {
383 include_once ABSPATH . '/wp-admin/includes/plugin.php';
384 }
385
386 add_action('wp_loaded', function(){
387
388 // external custom template location plugin no longer needed, deactivate it
389 deactivate_plugins( 'pdb-custom-templates.php', true );
390
391 if ( isset(Participants_Db::$plugin_options['enable_api']) && Participants_Db::$plugin_options['enable_api'] )
392 {
393 new \PDb_submission\rest_api\routing();
394 }
395 });
396
397 /*
398 * any plugins that require Participants Database should initialize on this action
399 * 'participants-database_activated'
400 */
401 if ( $activate ) {
402 do_action( self::PLUGIN_NAME . '_activated' );
403 }
404 }
405
406 /**
407 * provides a list of all plugin shortcodes
408 *
409 * @filter 'pdb-plugin_shortcode_list' array
410 *
411 * @return array list of shortcode tags
412 */
413 public static function plugin_shortcode_list()
414 {
415 return self::apply_filters( 'plugin_shortcode_list', array(
416 'pdb_record',
417 'pdb_signup',
418 'pdb_signup_thanks',
419 'pdb_update_thanks',
420 'pdb_request_link',
421 'pdb_list',
422 'pdb_single',
423 'pdb_search',
424 'pdb_total',
425 ) );
426 }
427
428 /**
429 * performs some needed character replacements
430 *
431 * this is to prevent wptexturize from thinking the < or > characters designate an HTML tag
432 *
433 * @param string $att_string the shortcode attributes string
434 * @return string the fixed attribute string
435 */
436 public static function fix_shortcode_special_chars( $att_string )
437 {
438 // hide angle brackets
439 $att_string = str_replace( array('<','>'), array('&lt;', '&gt;'), $att_string );
440
441 // straighten quotes
442 $att_string = PDb_List_Query::straighten_quotes( $att_string );
443
444 return $att_string;
445 }
446
447 /**
448 * filters the content to fix issues with quotes and angle brackets in shortcodes
449 *
450 * @param string $content
451 * @return string
452 */
453 public static function fix_shortcode_special_chars_content( $content )
454 {
455 if ( strpos( $content, '[pdb_list' ) !== false || strpos( $content, '[pdb_total' ) !== false ) {
456
457 preg_match_all('/\[(pdb_list|pdb_total)(.*?)\]/m', $content, $matches );
458
459 $replace_content = array();
460
461 foreach ( $matches[2] as $match_string ) {
462 $replace_content[] = self::fix_shortcode_special_chars( $match_string );
463 }
464
465 $content = str_replace( $matches[2], $replace_content, $content );
466
467 }
468
469 return $content;
470 }
471
472 /**
473 * sets up the database and options source names
474 *
475 * fired early on the 'plugins_loaded' hook
476 *
477 * @global \wpdb $wpdb
478 */
479 public static function setup_source_names()
480 {
481 /*
482 * these can be modified later with a filter hook
483 *
484 * this allows things like multilingual field definitions or possibly even multiple databases
485 *
486 * this must be in a plugin, a theme functions file will be too late!
487 */
488 global $wpdb;
489 $table_basename = $wpdb->prefix . str_replace( '-', '_', self::PLUGIN_NAME );
490 self::$participants_table = self::apply_filters( 'select_database_table', $table_basename );
491 self::$fields_table = self::apply_filters( 'select_database_table', $table_basename . '_fields' );
492 self::$groups_table = self::apply_filters( 'select_database_table', $table_basename . '_groups' );
493
494 // also filter the name of the settings to use
495 self::$participants_db_options = self::apply_filters( 'select_database_table', self::PLUGIN_NAME . '_options' );
496
497 /**
498 * @version 1.6.3
499 * @filter pdb-single_query
500 * @filter pdb-record_query
501 */
502 self::$single_query = self::apply_filters( 'single_query', 'pdb' );
503 self::$record_query = self::apply_filters( 'record_query', 'pid' );
504 }
505
506 /**
507 * runs any admin-only initializations
508 */
509 public static function admin_init()
510 {
511 // set up the fields update processor
512 new PDb_Manage_Fields_Updates();
513
514 // initialize the admin notices library
515 PAnD::init();
516
517 /**
518 * sets the admin notices class
519 *
520 * admin notices are set with:
521 *
522 * $notices = PDb_Admin_Notices::get_instance();
523 * $notices->error( 'error message text' );
524 */
525 PDb_Admin_Notices::get_instance();
526
527 // check the php version for possible warning
528 self::php_version_warning();
529
530 self::check_uploads_directory();
531
532 if ( is_admin() && array_key_exists( 'pdb-clear_sessions', $_GET ) ) {
533 PDb_submission\db_session::close_all();
534 }
535
536 new PDb_admin_list\mass_edit();
537 new PDb_admin_list\delete();
538
539 if ( self::plugin_setting_is_true( 'background_import' ))
540 {
541 new \PDb_import\import_status_display();
542 }
543 }
544
545 /**
546 * sets up the initial translations
547 */
548 public static function setup_translations()
549 {
550 self::load_plugin_textdomain( __FILE__ );
551
552 self::$plugin_title = self::apply_filters( 'plugin_title', __( 'Participants Database', 'participants-database' ) );
553
554 self::_set_i18n();
555 }
556
557 /**
558 * initializes the plugin in the WP environment
559 *
560 * fired on the 'plugins_loaded' hook
561 *
562 * @return null
563 */
564 public static function init()
565 {
566 /*
567 * instantiate the settings class; this only sets up the settings values,
568 * the WP Settings API may not be available at this point, so we register the
569 * settings UI on the 'admin_menu' hook
570 */
571 self::$Settings = new PDb_Settings();
572
573 if ( self::plugin_setting_is_true( 'use_session_alternate_method' ) ) {
574 add_filter( 'pdb-record_id_in_get_var', function () { return true; } );
575 }
576
577 /*
578 * set up the base reference object arrays
579 *
580 * this is to reduce the number of db queries
581 */
582 self::_setup_fields();
583
584 /**
585 * initialize the import background process
586 */
587 new \PDb_import\controller();
588
589 self::$plugin_title = 'Participants Database';
590 /**
591 * @version 1.6 filter pdb-private_id_length
592 */
593 self::$private_id_length = self::apply_filters( 'private_id_length', self::$private_id_length );
594
595 /*
596 * checks for the need to update the DB
597 *
598 * this is to allow for updates to occur in many different ways
599 */
600 if ( false === get_option( self::$db_version_option ) || get_option( self::$db_version_option ) != self::$db_version ) {
601 PDb_Init::on_update();
602 }
603
604 // gives us a way to update the fields to version 1.9.0 manually
605 add_action( 'admin_init', function () {
606 if ( array_key_exists( 'pdb-update-fields', $_GET ) ) {
607 PDb_Init::update_field_def_values();
608 }
609
610 if ( array_key_exists( 'pdb-remove-orphan-columns', $_GET ) ) {
611 PDb_Init::delete_orphan_columns();
612 }
613 });
614
615 if ( self::plugin_setting_is_true( 'html_email' ) ) {
616 $type = 'text/html; charset="' . get_option( 'blog_charset' ) . '"';
617 } else {
618 $type = 'text/plain; charset=us-ascii';
619 }
620 $email_headers = "From: " . self::plugin_setting_value('receipt_from_name') . " <" . self::plugin_setting_value('receipt_from_address') . ">\n" .
621 "Content-Type: " . $type . "\n";
622
623 self::$email_headers = self::apply_filters( 'email_headers', $email_headers );
624
625 new \PDb_shortcodes\search_return_link();
626
627 /**
628 * any plugins that require Participants Database settings/database should use this hook
629 *
630 * @version 1.6.3
631 * @action participants-database_initialized
632 */
633 do_action( self::PLUGIN_NAME . '_initialized' );
634 }
635
636 /**
637 * registers all scripts and stylesheets
638 */
639 public static function register_assets()
640 {
641 $min = self::use_minified_assets() ? '.min' : '';
642
643 /*
644 * register frontend scripts and stylesheets
645 */
646 wp_register_style( self::$prefix . 'frontend', plugins_url( "/css/participants-database$min.css", __FILE__ ), array('dashicons'), '1.8.3' );
647
648 if ( self::_set_custom_css() ) {
649 wp_register_style( 'custom_plugin_css', plugins_url( '/css/' . 'PDb-custom.css', __FILE__ ), null, self::$Settings->option_version() );
650 }
651
652 if ( self::_set_custom_print_css() ) {
653 wp_register_style( 'custom_plugin_print_css', plugins_url( '/css/' . 'PDb-custom-print.css', __FILE__ ), null, self::$Settings->option_version(), 'print' );
654 }
655
656 wp_add_inline_style(self::$prefix . 'frontend', self::inline_css() );
657
658 wp_register_script( self::$prefix . 'shortcode', self::asset_url( "js/shortcodes$min.js" ), array('jquery'), '1.2' );
659
660 wp_register_script( self::$prefix . 'list-filter', self::asset_url( "js/list-filter$min.js" ), array('jquery'), '2.0' );
661 wp_add_inline_script( self::$prefix . 'list-filter', self::inline_js_data( 'PDb_ajax', PDb_List::ajax_params() ), false );
662
663 wp_register_script( self::$prefix . 'otherselect', self::asset_url( "js/otherselect$min.js" ), array('jquery'), '0.6' );
664 }
665
666 /**
667 * checks the current page for a plugin shortcode and fires an action if found
668 *
669 * this function is fired on the 'wp' action
670 *
671 * action fired is: pdb-shortcode_present
672 *
673 * @global WP_Post $post
674 */
675 public static function check_for_shortcode()
676 {
677 static $shortcode_checked = false;
678 global $post;
679 /**
680 * @filter pdb-shortcode_in_content
681 * @param bool true if a plugin shortcode has been detected
682 * @param WP_Post object the current post
683 * @return bool true if plugin content is present
684 */
685 if ( $shortcode_checked === false && is_object( $post ) && self::apply_filters( 'shortcode_in_content', preg_match( '/(?<!\[)\[pdb_/', $post->post_content ) > 0, $post ) )
686 {
687 do_action( Participants_Db::$prefix . 'shortcode_present' );
688
689 self::$shortcode_present = true;
690
691 new \PDb_shortcodes\attributes();
692
693 $shortcode_checked = true;
694 }
695 }
696
697 /**
698 * processes the admin includes
699 *
700 * uses WP hook 'admin_enqueue_scripts'
701 *
702 * @param string $hook the admin menu hook as provided by the WP filter
703 * @return null
704 */
705 public static function admin_includes( $hook )
706 {
707 /*
708 * register admin scripts and stylesheets
709 */
710 $min = self::use_minified_assets() ? '.min' : '';
711
712 $manage_fields_handle = self::$prefix . 'manage_fields';
713
714 wp_register_script( self::$prefix . 'cookie', plugins_url( 'js/js.cookie-2.2.1.min.js', __FILE__ ), array('jquery'), '2.2.1' );
715 wp_register_script( $manage_fields_handle, self::asset_url( "js/manage_fields$min.js" ), array('jquery', 'jquery-ui-core', 'jquery-ui-tabs', 'jquery-ui-sortable', 'jquery-ui-dialog', self::$prefix . 'cookie'), '2.14', true );
716 wp_register_script( self::$prefix . 'settings_script', self::asset_url( "js/settings$min.js" ), array('jquery', 'jquery-ui-core', 'jquery-ui-tabs', self::$prefix . 'cookie'), self::$plugin_version . '.1', true );
717
718 wp_register_script( self::$prefix . 'record_edit_script', self::asset_url( "js/record_edit$min.js" ), array('jquery', 'jquery-ui-core', 'jquery-ui-tabs', self::$prefix . 'cookie'), self::$plugin_version.'-1.1', true );
719 wp_add_inline_script(self::$prefix.'record_edit_script', Participants_Db::inline_js_data( 'PDb_L10n', array(
720 'unsaved_changes' => __( "The changes you made will be lost if you navigate away from this page.", 'participants-database' ),
721 ), 'record_edit' ));
722
723 wp_register_script( 'jq-doublescroll', self::asset_url( "js/jquery.doubleScroll$min.js" ), array('jquery', 'jquery-ui-widget') );
724 wp_register_script( self::$prefix . 'admin', self::asset_url( "js/admin$min.js" ), array('jquery', 'jq-doublescroll', 'jquery-ui-sortable', self::$prefix . 'cookie', 'jquery-ui-dialog' ), self::$plugin_version );
725 wp_register_script( self::$prefix . 'otherselect', self::asset_url( "js/otherselect$min.js" ), array('jquery') );
726 wp_register_script( self::$prefix . 'list-admin', self::asset_url( "js/list_admin$min.js" ), array('jquery', 'jquery-ui-dialog'), '1.5.2' );
727 wp_register_script( self::$prefix . 'aux_plugin_settings_tabs', self::asset_url( "/js/aux_plugin_settings$min.js" ), array('jquery', 'jquery-ui-tabs', self::$prefix . 'admin', /*self::$prefix . 'jq-placeholder',*/ self::$prefix . 'cookie'), self::$plugin_version );
728 wp_register_script( self::$prefix . 'debounce', plugins_url( 'js/jq_debounce.js', __FILE__ ), array('jquery') );
729 wp_register_script( self::$prefix . 'admin-notices', self::asset_url( "js/pdb_admin_notices$min.js" ), array('jquery'), self::$plugin_version );
730 //wp_register_script( 'datepicker', plugins_url( 'js/jquery.datepicker.js', __FILE__ ) );
731 //wp_register_script( 'edit_record', plugins_url( 'js/edit.js', __FILE__ ) );
732 wp_register_script( self::$prefix . 'debug', self::asset_url( "js/pdb_debug$min.js" ), array('jquery'), self::$plugin_version );
733
734 // admin custom CSS
735 if ( self::_set_admin_custom_css() )
736 {
737 wp_register_style( 'custom_plugin_admin_css', plugins_url( '/css/PDb-admin-custom.css', __FILE__ ), array(self::$prefix . 'admin'), self::$Settings->option_version() );
738 }
739
740 // jquery UI theme
741 wp_register_style( self::$prefix . 'jquery-ui', self::asset_url( "css/jquery-ui-theme/jquery-ui.min.css" ) );
742 wp_register_style(self::$prefix . 'jquery-ui-structure', self::asset_url( "css/jquery-ui-theme/jquery-ui.structure.min.css" ) );
743 wp_register_style(self::$prefix . 'jquery-ui-theme', self::asset_url( "css/jquery-ui-theme/jquery-ui.pdb-theme$min.css" ), array(self::$prefix . 'jquery-ui',self::$prefix . 'jquery-ui-structure'), '2.0' );
744
745 wp_register_style( self::$prefix . 'utility', plugins_url( '/css/xnau-utility.css', __FILE__ ), null, self::$plugin_version );
746 wp_register_style( self::$prefix . 'global-admin', plugins_url( '/css/PDb-admin-global.css', __FILE__ ), false, self::$plugin_version );
747 wp_register_style( self::$prefix . 'frontend', plugins_url( '/css/participants-database.css', __FILE__ ), null, self::$plugin_version . '.1' );
748 wp_add_inline_style(self::$prefix . 'frontend', self::inline_css() );
749
750 wp_register_style( self::$prefix . 'admin', plugins_url( '/css/PDb-admin.css', __FILE__ ), false, self::$plugin_version . '1.3' );
751 wp_register_style( $manage_fields_handle, plugins_url( '/css/PDb-manage-fields.css', __FILE__ ), array( self::$prefix . 'admin' ), self::$plugin_version . '.1' );
752
753 if ( false !== stripos( $hook, 'participants-database' ) )
754 {
755 add_filter( 'admin_body_class', array(__CLASS__, 'add_admin_body_class') );
756
757 wp_enqueue_script( self::$prefix . 'admin' );
758 wp_enqueue_script( self::$prefix . 'otherselect' );
759
760 wp_enqueue_style(self::$prefix . 'jquery-ui-theme');
761 }
762
763 if ( $hook === 'toplevel_page_participants-database' )
764 {
765 new \PDb_admin_list\mass_edit_update();
766 }
767
768 if ( false !== stripos( $hook, 'participants-database_settings_page' ) )
769 {
770 wp_enqueue_script( self::$prefix . 'settings_script' );
771 }
772
773
774 if ( false !== stripos( $hook, 'participants-database_settings_page' ) || false !== stripos( $hook, 'participants-database-manage_fields' ) ) {
775
776 if ( !class_exists( '\_WP_Editors' ) ) {
777 require_once( ABSPATH . WPINC . '/class-wp-editor.php' );
778 }
779 \_WP_Editors::enqueue_default_editor();
780 }
781
782 if ( false !== stripos( $hook, 'participants-database-edit_participant' ) || false !== stripos( $hook, 'participants-database-add_participant' ) ) {
783 wp_enqueue_script(self::$prefix.'record_edit_script');
784 }
785
786 if ( false !== stripos( $hook, 'participants-database-manage_fields' ) || false !== stripos( $hook, 'pdb-participant_log_settings' ) ) {
787
788 wp_add_inline_script( $manage_fields_handle, Participants_Db::inline_js_data( 'manageFields', array('uri' => $_SERVER['REQUEST_URI']) ) );
789
790 wp_add_inline_script( $manage_fields_handle, Participants_Db::inline_js_data( 'PDb_L10n', array(
791 '_wpnonce' => wp_create_nonce(PDb_Manage_Fields_Updates::action_key),
792 'action' => PDb_Manage_Fields_Updates::action_key,
793 PDb_Session::id_var => self::$session->session_id(),
794 'loading_indicator' => Participants_Db::get_loading_spinner(),
795 'instance_index' => Participants_Db::$instance_index,
796 /* translators: don't translate the words in brackets {} */
797 'must_remove' => '<h4>' . __( 'You must remove all fields from the {name} group before deleting it.', 'participants-database' ) . '</h4>',
798 /* translators: don't translate the words in brackets {} */
799 'delete_confirm' => '<h4>' . __( 'Delete the {name} {thing}?', 'participants-database' ) . '</h4>',
800 'delete_confirm_field' => '<h4>' . __( 'Delete the selected field?', 'participants-database' ) . '</h4>',
801 'delete_confirm_fields' => '<h4>' . __( 'Delete the selected fields?', 'participants-database' ) . '</h4>',
802 'no_fields' => '<h4>' . __( 'No valid fields for this operation', 'participants-database' ) . '</h4>',
803 'unsaved_changes' => __( "The changes you made will be lost if you navigate away from this page.", 'participants-database' ),
804 'datatype_confirm' => '<h4 class="dashicons-before dashicons-info warning">' . __( 'Changing the form element on a field that has stored data can result in data loss.', 'participants-database' ) .'</h4><p><a href="https://wp.me/p48Sj5-Zb" target="_blank">' . __( 'More information here…', 'participants-database' ) . '</a></p>',
805 'datatype_confirm_button' => __( 'Yes, change the form element', 'participants-database' ),
806 'datatype_cancel_button' => __( 'No, don\'t change the form element', 'participants-database' ),
807 ), 'manage_fields' ) );
808 wp_enqueue_script( $manage_fields_handle );
809 wp_enqueue_style( $manage_fields_handle );
810 }
811
812 // global admin enqueues
813 wp_enqueue_style( 'pdb-global-admin' );
814 wp_enqueue_style( 'pdb-utility' );
815
816 // only incude these stylesheets on the plugin admin pages
817 if ( false !== stripos( $hook, 'participants-database' ) ) {
818 wp_enqueue_style( 'pdb-frontend' );
819 wp_enqueue_style( 'pdb-admin' );
820 wp_enqueue_style( 'custom_plugin_admin_css' );
821 }
822
823 if ( strpos( $hook, 'participants-database-upload_csv') !== false && self::plugin_setting_is_true( 'background_import' ) )
824 {
825 $handle = 'csv-status';
826 wp_register_script( $handle, self::asset_url( "js/csv_status$min.js" ), array('jquery'), '1.5' );
827
828 if ( filter_input( INPUT_POST, 'csv_file_upload', FILTER_DEFAULT, \Participants_Db::string_sanitize(FILTER_NULL_ON_FAILURE) ) )
829 {
830 do_action( 'pdb-csv_import_file_load' );
831 }
832
833 wp_add_inline_script( $handle, Participants_Db::inline_js_data('csvStatus', [
834 '_wpnonce' => wp_create_nonce( \PDb_import\import_status_display::action ),
835 'action' => \PDb_import\import_status_display::action,
836 'importing' => \PDb_import\import_status_display::is_importing(),
837 'dismiss' => \PDb_import\import_status_display::action . '-dismiss',
838 ] ) );
839
840 wp_enqueue_script($handle);
841 }
842 }
843
844 /**
845 * adds the enqueueing callback for scripts and stylesheets on the frontend
846 *
847 * this is triggered by the 'pdb-shortcode_present' hook
848 */
849 public static function add_shortcode_includes()
850 {
851 add_action( 'wp_enqueue_scripts', array(__CLASS__, 'include_assets'), 100 );
852 }
853
854 /**
855 * include frontend JS and CSS
856 *
857 * fired on WP hook 'wp_enqueue_scripts'
858 *
859 * @return null
860 */
861 public static function include_assets()
862 {
863 self::include_stylesheets();
864 self::add_scripts();
865 }
866
867 /**
868 * enqueues the frontend CSS
869 *
870 * @return null
871 */
872 public static function include_stylesheets()
873 {
874 if ( self::plugin_setting_is_true( 'use_plugin_css' ) ) {
875 wp_enqueue_style( 'pdb-frontend' );
876 }
877 wp_enqueue_style( 'custom_plugin_css' );
878 wp_enqueue_style( 'custom_plugin_print_css' );
879 }
880
881 /**
882 * enqueues the general plugin frontend scripts
883 *
884 * @return null
885 */
886 public static function add_scripts()
887 {
888 wp_enqueue_script( self::$prefix . 'shortcode' );
889 // wp_enqueue_script( self::$prefix . 'jq-placeholder' );
890 wp_enqueue_script( self::$prefix . 'otherselect' );
891 }
892
893 /**
894 * includes files for generating plugin admin pages
895 *
896 * grabs the name from the request and includes the file to display the page;
897 * this is the admin submenu callback
898 *
899 * @static
900 * @return null
901 */
902 public static function include_admin_file()
903 {
904 $file = str_replace( self::$plugin_page . '-', '', filter_input( INPUT_GET, 'page', FILTER_SANITIZE_SPECIAL_CHARS ) ) . '.php';
905
906 if ( is_file( plugin_dir_path( __FILE__ ) . $file ) ) {
907
908 // we'll need this in the included file
909 global $wpdb;
910
911 include $file;
912 }
913 }
914
915 /**
916 * registers this plugin's global events
917 *
918 * @param array $events
919 * @return array
920 */
921 public static function register_events( $events )
922 {
923 add_filter( 'pdb-translate_event_titles', [ __CLASS__,'translate_event_titles'] );
924
925 $events['pdb-view_single_record'] = 'View Single Record';
926 $events['pdb-open_record_edit'] = 'Open Edit Record Form';
927 $events['pdb-record_accessed_using_private_link'] = 'Record Accessed with Private Link';
928 $events['pdb-first_time_record_access_with_private_link'] = 'Record Accessed First Time with Private Link';
929
930 return $events;
931 }
932
933 /**
934 * provides the translated titles for the registered events
935 *
936 * @param array $events
937 * @return array
938 */
939 public static function translate_event_titles( $events )
940 {
941 $events['pdb-view_single_record'] = __( 'View Single Record', 'participants-database' );
942 $events['pdb-open_record_edit'] = __( 'Open Edit Record Form', 'participants-database' );
943 $events['pdb-record_accessed_using_private_link'] = __( 'Record Accessed with Private Link', 'participants-database' );
944 $events['pdb-first_time_record_access_with_private_link'] = __( 'Record Accessed First Time with Private Link', 'participants-database' );
945
946 return $events;
947 }
948
949 /**
950 * provides an URL for a single record
951 *
952 * @param int $id the record id
953 * @return string the URL
954 */
955 public static function single_record_url( $id )
956 {
957 if ( Participants_Db::plugin_setting_is_true('use_single_record_pid', false) ) {
958 $id = self::get_participant($id)['private_id'];
959 }
960
961 $page = self::add_uri_conjunction( self::single_record_page() ) . PDb_Single::single_query_var() . '=' . $id;
962 /**
963 * @filter pdb-single_record_url
964 * @param string single record page complete url
965 * @param int record id
966 * @return string URL to the record's single record page
967 */
968 return self::apply_filters( 'single_record_url', $page, $id );
969 }
970
971 /**
972 * provides the base URL fo the single record page
973 *
974 * @since 1.7.9
975 * @return string URL
976 */
977 public static function single_record_page()
978 {
979 /**
980 * @version 1.7
981 * @filter pdb-single_record_page sets the base page url of the single record page
982 * @param string single record page base url
983 * @return string URL
984 */
985 return self::apply_filters( 'single_record_page', self::get_permalink( self::plugin_setting_value('single_record_page') ) );
986 }
987
988 /**
989 * shows the frontend edit screen called by the [pdb_record] shortcode
990 *
991 *
992 * the ID of the record to show for editing can be provided one of three ways:
993 * $_GET['pid'] (private link) or in the POST array (actively editing a record)
994 * $atts['id'](deprecated) or $atts['record_id'] (in the shortcode), or
995 * self::$session->get('pdbid') (directly from the signup form)
996 *
997 *
998 * @param array $atts array of attributes drawn from the shortcode
999 * @return string the HTML of the record edit form
1000 */
1001 public static function print_record_edit_form( $atts )
1002 {
1003 // check for the ID in the shortcode first
1004 $record_id = PDb_Record::get_id_from_shortcode( $atts );
1005
1006 if ( $record_id === false )
1007 {
1008 // get the pid from the get string if given
1009 $get_pid = filter_input( INPUT_GET, Participants_Db::$record_query, FILTER_SANITIZE_SPECIAL_CHARS );
1010
1011 if ( empty( $get_pid ) ) {
1012 $get_pid = filter_input( INPUT_POST, Participants_Db::$record_query, FILTER_SANITIZE_SPECIAL_CHARS );
1013 }
1014
1015 if ( !empty( $get_pid ) ) {
1016 $record_id = self::get_participant_id( $get_pid );
1017 }
1018
1019 if ( $record_id === false ) {
1020 $record_id = self::$session->record_id( true );
1021 }
1022 }
1023
1024 $atts['record_id'] = $record_id;
1025
1026 return PDb_Record::print_form( $atts );
1027 }
1028
1029 /**
1030 * updates the "last_accessed" field in the database
1031 *
1032 * @param int $id the record to update
1033 * @global \wpdb $wpdb
1034 */
1035 private static function _record_access( $id )
1036 {
1037 global $wpdb;
1038
1039 $sql = 'UPDATE ' . self::$participants_table . ' SET `last_accessed` = "' . self::timestamp_now() . '" WHERE `id` = %s';
1040
1041 $wpdb->query( $wpdb->prepare( $sql, $id ) );
1042 }
1043
1044 /**
1045 * sets the last_accessed timestamp
1046 *
1047 * @param int $id id of the record to update
1048 */
1049 public static function set_record_access( $id )
1050 {
1051 if ( ! self::current_user_has_plugin_role( 'editor', __METHOD__ ) )
1052 {
1053 self::_record_access( $id );
1054 }
1055 }
1056
1057 /**
1058 * common function for printing all shortcodes
1059 *
1060 * @param array $params array of parameters passed in from the shortcode
1061 * @param string $content the content of the enclosure
1062 * @param string $tag the shortcode identification string
1063 * @return null
1064 */
1065 public static function print_shortcode( $params, $content, $tag )
1066 {
1067 /*
1068 * $params will be an empty string for a shortcode with no attributes, make
1069 * sure it's an array
1070 */
1071 $params = (array) $params;
1072 if ( ! empty( $content ) ) {
1073 $params['content'] = $content;
1074 }
1075 /**
1076 * @version 1.6
1077 *
1078 * 'pdb-shortcode_call_{$tag}' filter allows the shortcode attributes to be
1079 * altered before instantiating the shortcode object
1080 */
1081 $shortcode_parameters = self::apply_filters( 'shortcode_call_' . $tag, $params );
1082
1083 switch ( $tag ) {
1084 case 'pdb_record':
1085 return self::print_record_edit_form( $shortcode_parameters );
1086 break;
1087 case 'pdb_signup':
1088 return self::print_signup_form( $shortcode_parameters );
1089 break;
1090 case 'pdb_signup_thanks':
1091 case 'pdb_update_thanks':
1092 return self::print_signup_thanks_form( $shortcode_parameters );
1093 break;
1094 case 'pdb_request_link':
1095 return self::print_retrieval_form( $shortcode_parameters );
1096 break;
1097 case 'pdb_list':
1098 return self::print_list( $shortcode_parameters );
1099 break;
1100 case 'pdb_single':
1101 return self::print_single_record( $shortcode_parameters );
1102 break;
1103 case 'pdb_search':
1104 return self::print_search_form( $shortcode_parameters );
1105 break;
1106 case 'pdb_total':
1107 return self::print_total( $shortcode_parameters );
1108 break;
1109 }
1110 }
1111
1112 /**
1113 * prints a "total" value
1114 *
1115 * called by the "pdb_total" shortcode. this is to print a total number of records,
1116 * the number of records passing a filter, or an arithmetic sum of all the data
1117 * passing a filter.
1118 *
1119 * @param array $params the parameters passed in by the shortcode
1120 * @return string the output HTML
1121 */
1122 public static function print_total( $params )
1123 {
1124
1125 $params['module'] = 'total';
1126 $params['list_limit'] = -1;
1127
1128 return PDb_List::get_list( $params );
1129 }
1130
1131 /**
1132 * prints a single record called by [pdb_list] shortcode
1133 *
1134 * @param array $params the parameters passed in by the shortcode
1135 * @return string the output HTML
1136 */
1137 public static function print_list( $params )
1138 {
1139 $params['module'] = 'list';
1140
1141 return PDb_List::get_list( $params );
1142 }
1143
1144 /**
1145 * prints a list search form
1146 *
1147 * @param array $params the parameters passed in by the shortcode
1148 * @return string the output HTML
1149 */
1150 public static function print_search_form( $params )
1151 {
1152 $params = (array) $params + array('module' => 'search', 'search' => true);
1153
1154 return PDb_List::get_list( $params );
1155 }
1156
1157 /**
1158 * prints a single record called by [pdb_single] shortcode
1159 *
1160 * @param array $params the parameters passed in by the shortcode
1161 * @return string the output HTML
1162 */
1163 public static function print_single_record( $params )
1164 {
1165 // alias the 'id' attribute for backwards compatibility
1166 if ( isset( $params['id'] ) & !isset( $params['record_id'] ) ) {
1167 $params['record_id'] = $params['id'];
1168 unset( $params['id'] );
1169 }
1170
1171 return PDb_Single::print_record( $params );
1172 }
1173
1174 /**
1175 * prints a form from the Signup class
1176 *
1177 * @param array $params the parameters from the shortcode
1178 * @return string the output HTML
1179 */
1180 public static function print_signup_class_form( $params )
1181 {
1182 $params['post_id'] = get_the_ID();
1183
1184 return PDb_Signup::print_form( $params );
1185 }
1186
1187 /**
1188 * prints a signup form
1189 *
1190 * @param array $params the parameters passed in by the shortcode
1191 * @return string the output HTML
1192 */
1193 public static function print_signup_form( $params )
1194 {
1195 $params['module'] = 'signup';
1196
1197 return self::print_signup_class_form( $params );
1198 }
1199
1200 /**
1201 * prints the signup thanks form
1202 *
1203 * @param array $params the parameters passed in by the shortcode
1204 * @return string the output HTML
1205 */
1206 public static function print_signup_thanks_form( $params )
1207 {
1208 $params['module'] = 'thanks';
1209
1210 return self::print_signup_class_form( $params );
1211 }
1212
1213 /**
1214 * prints the private ID retrieval form
1215 *
1216 * @param array $params the parameters passed in by the shortcode
1217 * @return string the output HTML
1218 */
1219 public static function print_retrieval_form( $params )
1220 {
1221 $params['module'] = 'retrieve';
1222
1223 return self::print_signup_class_form( $params );
1224 }
1225
1226 /**
1227 * provides a set of all field definitions
1228 *
1229 * @return array of field definition objects indexed by fieldname
1230 */
1231 public static function all_field_defs()
1232 {
1233 $field_defs = wp_cache_get( PDb_Form_Field_Def::def_cache );
1234
1235 if ( ! $field_defs ) {
1236
1237 global $wpdb;
1238
1239 $sql = 'SELECT v.name, v.*, g.title AS grouptitle, g.id AS groupid, g.mode
1240 FROM ' . Participants_Db::$fields_table . ' v
1241 INNER JOIN ' . Participants_Db::$groups_table . ' g
1242 ON v.group = g.name
1243 ORDER BY v.order';
1244 $field_defs = $wpdb->get_results( $sql, OBJECT_K );
1245
1246 wp_cache_set( PDb_Form_Field_Def::def_cache, $field_defs, '', Participants_Db::cache_expire() );
1247 }
1248
1249 return $field_defs;
1250 }
1251
1252 /**
1253 * sets up the field definition array
1254 *
1255 * @global wpdb $wpdb
1256 */
1257 private static function _setup_fields()
1258 {
1259 self::_setup_fields_prop( self::all_field_defs() );
1260
1261 include_once self::$plugin_path . 'classes/PDb_fields/core.php';
1262
1263 // add our modular fields
1264 new \PDb_fields\initialize();
1265 }
1266
1267 /**
1268 * sets up the fields property
1269 *
1270 * @param array $field_defs array of fields definition objects
1271 */
1272 private static function _setup_fields_prop( $field_defs )
1273 {
1274 self::$fields = array();
1275
1276 foreach ( $field_defs as $field ) {
1277 self::$fields[$field->name] = new PDb_Form_Field_Def( $field->name );
1278 }
1279 }
1280
1281 /**
1282 * get all the attributes of a field by it's name
1283 *
1284 * an attribute or comma-separated list of attributes can be specified if not,
1285 * a default list of attributes is retrieved
1286 *
1287 * @global object $wpdb
1288 * @param string $field the name of the field to get
1289 * @param string $atts depricated
1290 * @return PDb_Form_Field_Def
1291 */
1292 public static function get_field_atts( $field = false, $atts = '*' )
1293 {
1294 return self::get_column( $field );
1295 }
1296
1297 /**
1298 * provides a field definition given the field name
1299 *
1300 * @param string $fieldname
1301 * @return PDb_Form_Field_Def|bool false if no field with the name
1302 */
1303 public static function get_field_def( $fieldname )
1304 {
1305 return self::get_column( $fieldname );
1306 }
1307
1308 /**
1309 * get an array of field groups
1310 *
1311 * @param string $column comma-separated list of columns to get, defaults to all (*)
1312 * @param mixed $exclude single group to exclude or array of groups to exclude
1313 * @return array returns an associative array column => value or indexed array
1314 * if only one column is specified in the $column argument
1315 */
1316 public static function get_groups( $column = '*', $exclude = false )
1317 {
1318 global $wpdb;
1319
1320 $where = ' WHERE `mode` IN ("' . implode( '","', array_keys( PDb_Manage_Fields::group_display_modes() ) ) . '")';
1321
1322 if ( $exclude ) {
1323
1324 $where .= ' AND `name` ';
1325
1326 if ( is_array( $exclude ) ) {
1327
1328 $where .= 'NOT IN ("' . implode( '","', $exclude ) . '") ';
1329 } else {
1330
1331 $where .= '!= "' . $exclude . '" ';
1332 }
1333 }
1334
1335 $sql = 'SELECT ' . $column . ' FROM ' . self::$groups_table . $where . ' ORDER BY `order`,`name` ASC';
1336
1337 $cachekey = md5( $sql );
1338
1339 $result = wp_cache_get( $cachekey );
1340
1341 if ( ! $result ) {
1342 $result = $wpdb->get_results( $sql, ARRAY_A );
1343
1344 wp_cache_set( $cachekey, $result, '', self::cache_expire() );
1345 }
1346
1347 // are we looking for only one column?
1348 // if so, flatten the array
1349 if ( $column !== '*' and false === strpos( $column, ',' ) ) {
1350
1351 $output = array();
1352
1353 foreach ( $result as $row ) {
1354 $output[] = $row[$column];
1355 }
1356
1357 return $output;
1358 } else {
1359
1360 $group_index = array();
1361
1362 // build an array indexed by the group's name
1363 foreach ( $result as $group )
1364 $group_index[$group['name']] = $group;
1365
1366 return $group_index;
1367 }
1368 }
1369
1370 /**
1371 * gets the names of all the persistent fields
1372 *
1373 * @return array of field names
1374 */
1375 public static function get_persistent()
1376 {
1377 return self::get_subset( 'persistent' );
1378 }
1379
1380 /**
1381 * gets a list of field names/titles
1382 *
1383 * assembles a list of columns from those columns set to display. Optionally,
1384 * a list of fields can be supplied with an array. This allows fields that are
1385 * not displayed to be included.
1386 *
1387 * as of 1.5 fields named in the $fields array don't need to have their 'sortable'
1388 * flag set in order to be included.
1389 *
1390 * @global wpdb $wpdb
1391 * @param string $type if 'sortable' will only select fields flagged as sortable
1392 * @param array $fields array of field names defining the fields listed for the
1393 * purpose of overriding the default selection
1394 * @param string $sort sorting method to use, can be 'order' which uses the
1395 * defined group/field order, 'column' which uses the
1396 * current display column order or 'alpha' which sorts the
1397 * list alphabetially; defaults to 'column'
1398 * @return array of form: title => name
1399 */
1400 public static function get_field_list( $type = false, $fields = false, $sort = 'column' )
1401 {
1402 global $wpdb;
1403
1404 $where_clauses = array();
1405 if ( $type == 'sortable' && !is_array( $fields ) ) {
1406 $where_clauses[] = 'f.sortable > 0';
1407 }
1408 if ( is_array( $fields ) ) {
1409 $where_clauses[] = 'f.name IN ("' . implode( '","', $fields ) . '")';
1410 } elseif ( !is_admin() ) {
1411 $where_clauses[] = 'f.display_column > 0 ';
1412 }
1413
1414 $where = empty( $where_clauses ) ? '' : "WHERE " . implode( ' AND ', $where_clauses );
1415
1416 switch ( $sort ) {
1417
1418 case 'alpha':
1419 $sql = "
1420 SELECT f.name, REPLACE(f.title,'\\\','') AS title
1421 FROM " . self::$fields_table . " f
1422 " . $where . "
1423 ORDER BY f.name";
1424 break;
1425
1426 case 'column':
1427 $column = (is_admin() ? 'admin_column' : 'display_column');
1428 $sql = "
1429 SELECT f.name, REPLACE(f.title,'\\\','') AS title
1430 FROM " . self::$fields_table . " f
1431 $where
1432 ORDER BY CASE WHEN f.$column = 0 THEN f.order END ASC, f.$column ASC";
1433 break;
1434
1435 case 'order':
1436 default:
1437 $sql = "
1438 SELECT f.name, REPLACE(f.title,'\\\','') AS title, g.order
1439 FROM " . self::$fields_table . " f
1440 INNER JOIN " . self::$groups_table . " g ON f.group = g.name
1441 " . $where . "
1442 ORDER BY g.order, f.order";
1443 break;
1444 }
1445
1446 $result = $wpdb->get_results( $sql, ARRAY_N );
1447
1448 // construct an array of this form: title => name
1449 $return = array();
1450 foreach ( $result as $item ) {
1451 if ( isset( $return[$item[1]] ) ) {
1452 $key = self::title_key( $item[1], $item[0] );
1453 } else {
1454 $key = self::title_key( $item[1] );
1455 }
1456 $return[$key] = $item[0];
1457 }
1458 return $return;
1459 }
1460
1461 /**
1462 * get the names of all the sortable fields
1463 *
1464 * this checks the "sortable" column and collects the list of sortable columns
1465 * from those columns set to display. Optionally, a list of fields to include
1466 * can be supplied with an array. This allows fields that are not displayed to
1467 * be included.
1468 *
1469 * @param array $fields array of field names defining the fields listed for the
1470 * purpose of overriding the default selection
1471 * @param string $sort sorting method to use, can be 'order' which uses the
1472 * defined group/field order, 'column' which uses the
1473 * current display column order or 'alpha' which sorts the
1474 * list alphabetially; defaults to 'column'
1475 * @param return array
1476 */
1477 public static function get_sortables( $fields = false, $sort = 'column' )
1478 {
1479 return self::get_field_list( 'sortable', $fields, $sort );
1480 }
1481
1482 /**
1483 * gets a subset of field names
1484 *
1485 * this function only works for boolean qualifiers or "column order" columns where
1486 * any number greater than 0 indicates the field is to be displayed in a column
1487 *
1488 * @param string the name of the qualifier to use to select a set of field names
1489 * @return array an indexed array of field names
1490 */
1491 private static function get_subset( $subset )
1492 {
1493
1494 global $wpdb;
1495
1496 $sql = "
1497 SELECT `name`
1498 FROM " . self::$fields_table . "
1499 WHERE `" . $subset . "` > 0";
1500
1501 $result = $wpdb->get_results( $sql, ARRAY_N );
1502
1503 // get the 2nd dimension of the array
1504 $return = array();
1505 foreach ( $result as $item )
1506 $return[] = $item[0];
1507
1508 return $return;
1509 }
1510
1511 /**
1512 * gets a single column object
1513 *
1514 * @param string $name the column name
1515 * @return PDb_Form_Field_Def|bool false if no field defined for the given name
1516 */
1517 public static function get_column( $name )
1518 {
1519 return isset( self::$fields[$name] ) ? self::$fields[$name] : false;
1520 }
1521
1522 /**
1523 * checks a string against active columns to validate input
1524 *
1525 * @param string $string the name to test
1526 * @return bool
1527 */
1528 public static function is_column( $string )
1529 {
1530 return isset( self::$fields[$string] );
1531 }
1532
1533 /**
1534 * checks a string against defined groups to validate a group name
1535 *
1536 * @global wpdb $wpdb
1537 * @param string $string the name to test
1538 * @return bool
1539 */
1540 public static function is_group( $string )
1541 {
1542 global $wpdb;
1543
1544 $sql = 'SELECT COUNT(*)
1545 FROM ' . self::$groups_table . ' g
1546 WHERE g.name = %s
1547 AND g.mode IN ("admin","private","public")';
1548
1549 $count = $wpdb->get_var( $wpdb->prepare( $sql, trim( $string ) ) );
1550
1551 return $count > 0;
1552 }
1553
1554 /**
1555 * gets a set of field attributes as filtered by context
1556 *
1557 * @global wpdb $wpdb
1558 * @param string|array $filter sets the context of the display and determines the
1559 * set of columns to return, also accepts an array of
1560 * column names
1561 * @return object the object is ordered first by the order of the group, then
1562 * by the field order
1563 */
1564 public static function get_column_atts( $filter = 'new' )
1565 {
1566 return PDb_submission\main_query\columns::field_definition_list( $filter );
1567 }
1568
1569 /**
1570 * provides a list of main db columns
1571 *
1572 * @global wpdb $wpdb
1573 * @return array of column names
1574 */
1575 public static function table_columns()
1576 {
1577 $tablename = Participants_Db::participants_table();
1578 $cachekey = 'pdb-table-' . $tablename . '-columns';
1579
1580 $columns = wp_cache_get($cachekey);
1581
1582 if ( ! $columns ) {
1583 global $wpdb;
1584 $shown_columns = $wpdb->get_results( 'SHOW COLUMNS FROM ' . $tablename );
1585
1586 $columns = array();
1587 foreach( $shown_columns as $column ) {
1588 $columns[] = $column->Field;
1589 }
1590 wp_cache_set( $cachekey, $columns, '', Participants_Db::cache_expire() );
1591 }
1592
1593 return $columns;
1594 }
1595
1596 /**
1597 * builds an object of all participant values structured by groups and columns
1598 *
1599 * TODO: this function is DEPRICATED in favor of using the Shortcode class to render
1600 * shortcode output, but we have to leave it in here for the moment because
1601 * there may be modified templates using this function still in use
1602 *
1603 * @param string $id the id number of the record
1604 * @param array $exclude an array of fields to ecplude
1605 * @return object containing all the field and their values, ordered by groups
1606 */
1607 public static function single_record_fields( $id, $exclude = '' )
1608 {
1609
1610 global $wpdb;
1611
1612 // get the groups object
1613 $sql = '
1614 SELECT g.title, g.name, g.description
1615 FROM ' . self::$groups_table . ' g
1616 WHERE g.display = 1
1617 ORDER BY `order` ASC
1618 ';
1619
1620 $groups = $wpdb->get_results( $sql, OBJECT_K );
1621
1622 if ( is_array( $exclude ) ) {
1623
1624 $excludes = "AND v.name NOT IN ('" . implode( "','", $exclude ) . "') ";
1625 } else
1626 $excludes = '';
1627
1628 // add the columns to each group
1629 foreach ( $groups as $group ) {
1630
1631 $group->fields = $wpdb->get_results( 'SELECT v.name, v.title, v.form_element
1632 FROM ' . self::$fields_table . ' v
1633 WHERE v.group = "' . $group->name . '"
1634 ' . $excludes . '
1635 AND v.form_element != "hidden"
1636 ORDER BY v.order
1637 ', OBJECT_K );
1638
1639 // now get the participant value for the field
1640 foreach ( $group->fields as $field ) {
1641
1642 $field->value = current( $wpdb->get_row( "SELECT `" . $field->name . "`
1643 FROM " . self::$participants_table . "
1644 WHERE `id` = '" . $id . "'", ARRAY_N ) );
1645 } // fields
1646 }// groups
1647
1648 return $groups;
1649 }
1650
1651 /**
1652 * processes a form submit
1653 *
1654 * this processes all record form submissions front- and back-end
1655 *
1656 * @global wpdb $wpdb
1657 *
1658 * @param array $post the array of new values (typically the $_POST array)
1659 * @param string $action the db action to be performed: insert or update
1660 * @param int|bool $record_id the id of the record to update. If it is false
1661 * or omitted, it creates a new record, if true, it
1662 * creates or updates the default record.
1663 * @param array|bool $column_names array of column names to process from the $post
1664 * array, if false, processes a preset set of columns
1665 * @param bool $func_call optional flag to indicate the method is getting called by external code
1666 * @param string $context optional label
1667 *
1668 * @return int|bool int ID of the record created or updated, bool false if submission
1669 * does not validate
1670 */
1671 public static function process_form( $post, $action, $record_id = false, $column_names = false, $func_call = false, $context = '' )
1672 {
1673 do_action( 'pdb-clear_page_cache', isset( $post['shortcode_page'] ) ? $post['shortcode_page'] : $_SERVER['REQUEST_URI'] );
1674
1675 // make sure we are getting the right arguments #3185
1676 if ( is_string( $func_call ) )
1677 {
1678 $context = $func_call;
1679 $func_call = false;
1680 }
1681
1682 $record_match = \PDb_submission\matching\record::get_object( $post, $record_id );
1683 /** @var PDb_submission\matching\record $record_match */
1684
1685 // modify the action according the the match mode
1686 $action = $record_match->get_action( $action );
1687
1688 if ( $action === 'skip' ) {
1689 return false;
1690 }
1691
1692 // get the record id to use in the query
1693 $record_id = $record_match->record_id();
1694
1695 // check the captcha if present before going any further
1696 $captcha_field = '';
1697
1698 if ( PDb_CAPTCHA::captcha_field_name( $column_names ) )
1699 {
1700 $captcha_field = PDb_CAPTCHA::captcha_field_name( array_keys( $post ) );
1701
1702 if ( false === $captcha_field )
1703 {
1704 // captcha field was expected but missing
1705 return false;
1706 }
1707
1708 self::$validation_errors->validate( self::deep_stripslashes( $post[$captcha_field] ), self::$fields[$captcha_field], $post, $record_id );
1709
1710 if ( self::$validation_errors->errors_exist() )
1711 {
1712 // captcha didn't validate
1713 return false;
1714 }
1715 }
1716
1717 /*
1718 * upload any files included in the form submission
1719 *
1720 * the validated file names are placed in the $post array
1721 */
1722 $post = PDb_File_Uploads::process_submission_uploads( $post );
1723
1724 // set the insert status value
1725 self::$insert_status = $action;
1726
1727 $context_label = empty( $context ) ? 'main process_form method' : $context;
1728
1729 $main_query = PDb_submission\main_query\base_query::get_instance( $action, $post, $record_id, $func_call, $context );
1730 /** @var \PDb_submission\main_query\base_query $main_query */
1731
1732 /*
1733 * process the submitted data
1734 */
1735 foreach ( $main_query->column_array( $column_names ) as $column ) {
1736
1737 /** @var object $column */
1738
1739 /**
1740 * @action pdb-process_form_submission_column_{$fieldname}
1741 *
1742 * @param object $column the current column
1743 * @param array $post the current post array
1744 *
1745 */
1746 do_action( 'pdb-process_form_submission_column_' . $column->name, $column, $post );
1747
1748 $field = PDb_submission\main_query\columns::get_column_object( $column, $main_query->column_value( $column->name ) );
1749
1750 /*
1751 * if $column_names is false, validate all fields except the captcha field which has already been validated
1752 * if $column_nmaes is an array, only validate fields that are in that array except the captcha field
1753 */
1754 if ( ( $column_names === false || in_array( $column->name, $column_names ) ) && $column->name !== $captcha_field )
1755 {
1756 $main_query->validate_column( $field, $column );
1757 }
1758
1759 if ( $field->add_to_query( $action ) ) {
1760
1761 // add the column to the query
1762 $main_query->add_column( $field->value(), $field->query_clause() );
1763 }
1764 } // columns
1765
1766 /*
1767 * now that we're done adding the submitted data to the query, check for
1768 * validation and abort the process if there are validation errors
1769 */
1770 if ( $main_query->has_validation_errors() ) {
1771 return false;
1772 }
1773
1774 $updated_record_id = $main_query->execute_query();
1775
1776 PDb_Participant_Cache::clear_cache( $updated_record_id );
1777
1778 return $updated_record_id;
1779 }
1780
1781 /**
1782 * provides a truncated database error message
1783 *
1784 * @param string the full error message
1785 * @return string
1786 */
1787 public static function db_error_message( $message )
1788 {
1789 return rtrim( stristr( $message, 'on query:', true ), 'on query:' );
1790 }
1791
1792 /**
1793 * parses the markdown string used to store the values for a link form element
1794 *
1795 * will also accept a bare URL. If the supplied string or URL does not validate
1796 * as an URL, return the string
1797 *
1798 * @param string $markdown_string
1799 * @return array URL, linktext
1800 */
1801 public static function get_link_array( $markdown_string )
1802 {
1803 if ( preg_match( '#^<([^>]+)>$#', trim( $markdown_string ), $matches ) ) {
1804 return array($matches[1], '');
1805 } elseif ( preg_match( '#^\[([^\]]+)\]\(([^\)]+)\)$#', trim( $markdown_string ), $matches ) ) {
1806 $url = self::valid_url( $matches[2] ) ? $matches[2] : '';
1807 return array($url, $matches[1]);
1808 } else
1809 return self::valid_url( $markdown_string ) ? array($markdown_string, '') : array('', $markdown_string);
1810 }
1811
1812 /**
1813 * validates a URL
1814 *
1815 * @param string $url
1816 * @return bool true if the URL is valid
1817 */
1818 public static function valid_url( $url ) {
1819 /**
1820 * provides an way to add a custom URL validation
1821 *
1822 * @filter pdb-validate_url
1823 * @param bool validation result
1824 * @param string URL
1825 * @return bool
1826 */
1827 return self::apply_filters('validate_url', filter_var( $url, FILTER_VALIDATE_URL, FILTER_NULL_ON_FAILURE ) !== false, $url );
1828 }
1829
1830 /**
1831 * gets the default set of values
1832 *
1833 * @version 1.7.9.8 dynamic hidden fields are included with empty value
1834 * @version 1.6 placeholder elements are excluded
1835 *
1836 * @global wpdb $wpdb
1837 * @param bool $add_persistent if true, adds persistent field data
1838 * @return array name=>value
1839 */
1840 public static function get_default_record( $add_persistent = true )
1841 {
1842 $cachekey = 'pdb-default_record';
1843 $default_record = wp_cache_get( $cachekey );
1844
1845 if ( ! $default_record ) {
1846
1847 $default_record = array();
1848
1849 foreach ( self::$fields as $fieldname => $field ) {
1850 /** @var PDb_Form_Field_Def $field */
1851
1852 // skip fields that don't have a stored value in the main database
1853 if ( ! in_array( $field->name(), Participants_Db::table_columns() ) ) {
1854 continue;
1855 }
1856
1857 $default_record[$fieldname] = $field->default_display();
1858
1859 }
1860
1861 // get the id of the last record stored
1862 $prev_record_id = get_transient( self::$last_record );
1863
1864 if ( $add_persistent && self::is_admin() && $prev_record_id ) {
1865
1866 $previous_record = self::get_participant( $prev_record_id );
1867
1868 if ( $previous_record ) {
1869
1870 $persistent_fields = self::get_persistent();
1871
1872 foreach ( $persistent_fields as $persistent_field ) {
1873
1874 if ( !empty( $previous_record[$persistent_field] ) ) {
1875
1876 $default_record[$persistent_field] = $previous_record[$persistent_field];
1877 }
1878 }
1879 }
1880 }
1881
1882 $default_record['private_id'] = self::generate_pid();
1883
1884 // #2797 don't include timestamps in default record
1885 // PDb_Date_Display::reassert_timezone();
1886 // $default_record['date_recorded'] = \Participants_Db::timestamp_now();
1887 // $default_record['date_updated'] = \Participants_Db::timestamp_now();
1888
1889 wp_cache_set( $cachekey, $default_record, '', Participants_Db::cache_expire() );
1890 }
1891
1892 return $default_record;
1893 }
1894
1895 /**
1896 * gets the data for a record from a cached dataset
1897 *
1898 * this is optimized for operations that might require this to be called multiple
1899 * times, it loads 100 (filtered value) records into the cache, and only performs
1900 * the load if the request is for a record outide of the cached range. That range
1901 * is then cached as well.
1902 *
1903 * @global object $wpdb
1904 * @param string|bool $id the record ID; returns default record if omitted or bool false
1905 *
1906 * @return array|bool associative array of name=>value pairs; false if no record
1907 * matching the ID was found
1908 */
1909 public static function get_participant( $id )
1910 {
1911 if ( ! $id ) {
1912 return self::get_default_record();
1913 }
1914
1915 $record = false;
1916
1917 /**
1918 * provides a way to bypass the cache when getting a participant record
1919 *
1920 * @filter pdb-use_participant_caching
1921 * @param bool
1922 * @return bool
1923 */
1924 if ( self::apply_filters( 'use_participant_caching', true ) ) {
1925 $record = PDb_Participant_Cache::get_participant( $id );
1926 }
1927
1928 if ( ! $record ) {
1929 $record = self::_get_participant( $id );
1930 }
1931
1932 return $record;
1933 }
1934
1935 /**
1936 * gets an array of record values
1937 *
1938 * @global wpdb $wpdb
1939 * @param int $id the record ID
1940 *
1941 * @return array|bool associative array of name=>value pairs; false if no record
1942 * matching the ID was found
1943 */
1944 private static function _get_participant( $id )
1945 {
1946 global $wpdb;
1947
1948 //self::debug_log(__METHOD__.' cache missed, using fallback method for record: ' . $id, 3);
1949
1950 $sql = 'SELECT p.' . implode( ',p.', self::db_field_list() ) . ' FROM ' . self::participants_table() . ' p WHERE p.id = %d';
1951
1952 $result = $wpdb->get_row( $wpdb->prepare( $sql, $id ), ARRAY_A );
1953
1954 if ( is_array( $result ) ) {
1955 return array_merge( $result, array('id' => $id) );
1956 } else {
1957 return false;
1958 }
1959 }
1960
1961 /**
1962 * gets a participant id by private ID
1963 *
1964 * @param string $pid the private ID for a record
1965 *
1966 * @return int|bool the record ID or false
1967 *
1968 */
1969 public static function get_participant_id( $pid )
1970 {
1971
1972 return self::_get_participant_id_by_term( 'private_id', $pid );
1973 }
1974
1975 /**
1976 * finds the ID of a record given the value of one of it's fields.
1977 *
1978 * Returns the first of multiple matches
1979 *
1980 * @param string $term the name of the field to use in matching the record
1981 * @param string $value the value to match
1982 * @param bool $single if true, only return a match if it is unique
1983 * @return int|bool false if no valid id found
1984 */
1985 public static function get_record_id_by_term( $term, $value, $single = true )
1986 {
1987 return self::_get_participant_id_by_term( $term, $value, $single );
1988 }
1989
1990 /**
1991 * gets a participant record id by term
1992 *
1993 * given an identifier, returns the id of the record identified. If there is
1994 * more than one record with the given term, returns the first one.
1995 *
1996 * @global wpdb $wpdb
1997 * @param string $term the column to match
1998 * @param string $value the value to search for
1999 * @param bool $single if true, return only one ID
2000 *
2001 * @return int|array|bool returns integer if one match, array of integers if multiple
2002 * matches (and single is false), false if no match
2003 */
2004 private static function _get_participant_id_by_term( $term, $value, $single = true )
2005 {
2006 if ( $value === false || is_null( $value ) || !self::is_column( $term ) )
2007 return false;
2008
2009 $found = false;
2010 $cachekey = 'pdb-record_by_term_' . $term . $value;
2011 $output = wp_cache_get( $value, $cachekey, false, $found );
2012
2013 if ( ! $found ) {
2014 global $wpdb;
2015
2016 $sql = 'SELECT p.id FROM ' . self::$participants_table . ' p WHERE p.' . $term . ' = %s';
2017 $result = $wpdb->get_results( $wpdb->prepare( $sql, $value ), ARRAY_N );
2018
2019 if ( !is_array( $result ) ) {
2020 $output = false;
2021 } else {
2022 $output = array();
2023
2024 foreach ( $result as $id ) {
2025 $output[] = current( $id );
2026 }
2027 }
2028 wp_cache_set($value, $output, $cachekey, self::cache_expire() );
2029 }
2030
2031 if ( $output === false ) {
2032 return false;
2033 } else {
2034 return $single ? current( $output ) : $output;
2035 }
2036 }
2037
2038 /**
2039 * provides the length of the private ID
2040 *
2041 * @return int
2042 */
2043 public static function private_id_length()
2044 {
2045 return self::apply_filters( 'private_id_length', self::$private_id_length );
2046 }
2047
2048 /**
2049 * generates a 5-character private ID
2050 *
2051 * the purpose here is to create a unique yet managably small and unguessable
2052 * (within reason) id number that can be included in a link to call up a
2053 * specific record by a user.
2054 *
2055 * @return string unique alphanumeric ID
2056 */
2057 public static function generate_pid()
2058 {
2059
2060 $pid = '';
2061
2062 $chr_source = str_split('1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ');
2063
2064 for ( $i = 0; $i < self::private_id_length(); $i++ ) {
2065
2066 $pid .= $chr_source[array_rand( $chr_source )];
2067 }
2068
2069 $pid = self::apply_filters( 'generate_private_id', $pid );
2070
2071 // if by chance we've generated a string that has been used before, generate another
2072 return self::_id_exists( $pid, 'private_id' ) ? self::generate_pid() : $pid;
2073 }
2074
2075 /**
2076 * tests for existence of record in main db
2077 *
2078 * @global object $wpdb
2079 * @param string $id the identifier to test
2080 * @param string $field the db field to test the $id value against
2081 * @return bool true if a record mathing the criterion exists
2082 */
2083 private static function _id_exists( $id, $field = 'id' )
2084 {
2085
2086 global $wpdb;
2087
2088 $id_exists = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM " . self::$participants_table . " p WHERE p." . $field . " = '%s' LIMIT 1", $id ) );
2089
2090 if ( NULL !== $id_exists )
2091 return $id_exists === '0' ? false : true;
2092 else {
2093 return false;
2094 }
2095 }
2096
2097 /**
2098 * returns the next valid record id from the results of the admin list filter
2099 *
2100 * the next id can be the next higher or lower. This function will wrap, so it
2101 * always returns a valid id.
2102 *
2103 * @global \wpdb $wpdb
2104 * @param string $current_id the current id
2105 * @param bool $increment true for next higher, false for next lower
2106 * @return string the next valid id
2107 */
2108 public static function next_id( $current_id, $increment = true )
2109 {
2110 $inc_value = $increment ? 1 : -1;
2111
2112 /**
2113 * @filter pdb-admin_edit_record_next_id
2114 * @param bool false initial state
2115 * @param int $current_id
2116 * @param int $inc_value the increment value
2117 * @return bool|int the next record ID, bool false if not
2118 */
2119 $id = Participants_Db::apply_filters( 'admin_edit_record_next_id', false, $current_id, $inc_value );
2120
2121 if ( is_numeric( $id ) )
2122 {
2123 return $id;
2124 }
2125
2126 $id = $current_id;
2127
2128 $query = get_transient( PDb_admin_list\query::query_store );
2129
2130 if ( $query ) // get the ID from the last list filter
2131 {
2132 $result_list = PDb_admin_list\record_list_cache::get();
2133
2134 if ( false === $result_list )
2135 {
2136 $result_list = PDb_admin_list\query::result_list( $query );
2137
2138 PDb_admin_list\record_list_cache::set( $result_list );
2139 }
2140
2141 $index = array_search( $id, $result_list );
2142
2143 $next_i = $index + $inc_value;
2144
2145 if ( ! array_key_exists( $next_i, $result_list ) )
2146 {
2147 $next_i = $inc_value === 1 ? array_key_first( $result_list ) : array_key_last( $result_list );
2148 }
2149
2150 return $result_list[ $next_i ];
2151 }
2152 else // get it from the full ID list
2153 {
2154 global $wpdb;
2155 $max = $wpdb->get_var( 'SELECT MAX(p.id) FROM ' . self::$participants_table . ' p' );
2156 $id = (int) $id;
2157 $id = $id + $inc_value;
2158
2159 while ( !self::_id_exists( $id ) )
2160 {
2161 $id = $id + $inc_value;
2162
2163 if ( $id > $max )
2164 {
2165 $id = 1;
2166 }
2167 elseif ( $id < 1 )
2168 {
2169 $id = $max;
2170 }
2171 }
2172 return $id;
2173 }
2174 }
2175
2176 /**
2177 * tests for the presence of an email address in the records
2178 *
2179 * @param string $email the email address to search for
2180 * @return boolean true if email is found
2181 */
2182 public static function email_exists( $email )
2183 {
2184 if ( Participants_Db::plugin_setting_is_set('primary_email_address_field') ) {
2185 return self::_id_exists( $email, Participants_Db::plugin_setting('primary_email_address_field') );
2186 } else
2187 return false;
2188 }
2189
2190 /**
2191 * tells if there is a record in the db with a matching value
2192 *
2193 * @param string $value the value of the field to test
2194 * @param string $field the field to test
2195 * @param int $mask_id optional record id to exclude
2196 * @return bool true if there is a matching value for the field
2197 */
2198 public static function field_value_exists( $value, $field, $mask_id = 0 )
2199 {
2200 return \PDb_submission\matching\record::field_value_exists($value, $field, $mask_id);
2201 }
2202
2203 /**
2204 * prepares a serialized array for display
2205 *
2206 * displays an array as a series of comma-separated strings
2207 *
2208 * @param string|array $array of field options or attributes
2209 * @return string the prepared string
2210 */
2211 public static function array_to_string_notation( $array )
2212 {
2213 return PDb_Manage_Fields_Updates::array_to_string_notation($array);
2214 }
2215
2216 /**
2217 * adds a blank field to the field definitions
2218 *
2219 * @global wpdb $wpdb
2220 * @param array $params the setup parameters for the new field
2221 * @return boolean
2222 */
2223 public static function add_blank_field( $params )
2224 {
2225 // prevent spurious field creation
2226 if ( !isset( $params['name'] ) || empty( $params['name'] ) ) return;
2227
2228 global $wpdb;
2229
2230 // remove any invalid columns
2231 $params = array_intersect_key( $params, self::fields_table_columns() );
2232
2233 // set up the params with needed default values
2234 $field_parameters = wp_parse_args( $params, array('form_element' => 'text-line') );
2235
2236 // check for a duplicate field
2237 $field_check = $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM ' . Participants_Db::$fields_table . ' WHERE `name` = %s', $field_parameters['name'] ) );
2238
2239 if ( $field_check != '0' ) {
2240
2241 if ( PDB_DEBUG ) {
2242 PDb_Admin_Notices::post_error(' failed to add field "' . $params['name'] . '": possibly a duplicate', __METHOD__, false );
2243 self::debug_log( __METHOD__ . ' failed to add row ' . $params['name'] . ': duplicate field' );
2244 }
2245
2246 return false;
2247 }
2248
2249 foreach( array( 'options', 'attributes' ) as $prop ) {
2250 if ( isset( $field_parameters[$prop] ) ) {
2251
2252 // skip if it's already a serialized array
2253 if ( is_serialized ( $field_parameters[$prop] ) ) {
2254 break;
2255 }
2256
2257 // convert the property value to a serialized array
2258 $field_parameters[$prop] = serialize( (array) $field_parameters[$prop] );
2259 }
2260 }
2261
2262 $wpdb->insert( self::$fields_table, $field_parameters );
2263
2264 if ( $wpdb->last_error ) {
2265
2266 if ( PDB_DEBUG ) {
2267 PDb_Admin_Notices::post_error(' error when adding field "' . $params['name'] . '": ' . $wpdb->last_error, __METHOD__, false );
2268 self::debug_log( __METHOD__ . ' failed to add row ' . $params['name'] . ' with error: ' . $wpdb->last_error );
2269 }
2270
2271 return false;
2272 }
2273
2274 // if this column does not exist in the DB, add it
2275 if ( count( $wpdb->get_results( "SHOW COLUMNS FROM `" . self::participants_table() . "` LIKE '" . $field_parameters['name'] . "'", ARRAY_A ) ) < 1 ) {
2276
2277 if ( false === ( self::_add_db_column( $field_parameters ) ) ) {
2278
2279 if ( PDB_DEBUG ) {
2280 PDb_Admin_Notices::post_error(' failed to add database column "' . $params['name'] . '" with error: ' . $wpdb->last_error, __METHOD__, true );
2281 self::debug_log( __METHOD__ . ' failed to add column with error: ' . $wpdb->last_error .' data: ' . print_r( $field_parameters, true ) );
2282 }
2283
2284 return false;
2285 }
2286 }
2287
2288 self::debug_log( __METHOD__ . ' field added: "' . $params['name'] . '"' );
2289
2290 /**
2291 * @action pdb-new_field_added
2292 * @param array of initial field parameters
2293 */
2294 do_action( Participants_Db::$prefix . 'new_field_added', $field_parameters );
2295
2296 return true;
2297 }
2298
2299 /**
2300 * adds a new column (field) to the database
2301 *
2302 * @global object $wpdb
2303 * @param array $atts a set of attributes to define the new columns
2304 * @retun bool success of the operation
2305 */
2306 private static function _add_db_column( $atts )
2307 {
2308 global $wpdb;
2309
2310 $datatype = PDb_FormElement::get_datatype( $atts );
2311
2312 // an empty or false datatype skips adding the field's column to the main table
2313 if ( ! $datatype ) {
2314
2315 return true;
2316 }
2317
2318 $sql = 'ALTER TABLE `' . self::participants_table() . '` ADD `' . $atts['name'] . '` ' . $datatype . ' NULL';
2319
2320 return $wpdb->query( $sql );
2321 }
2322
2323 /**
2324 * provides an array of column info from the field def table
2325 *
2326 * @global wpdb $wpdb
2327 * @return array as $name => $type
2328 */
2329 public static function fields_table_columns()
2330 {
2331 global $wpdb;
2332 $columns = array();
2333
2334 foreach( $wpdb->get_results('SHOW COLUMNS FROM ' . self::$fields_table ) as $column ) {
2335 $columns[$column->Field] = $column->Type;
2336 }
2337
2338 return $columns;
2339 }
2340
2341 /**
2342 * processes any POST requests
2343 *
2344 * this is called on the 'init' hook
2345 *
2346 * @global wpdb $wpdb
2347 * @return null
2348 */
2349 public static function process_page_request()
2350 {
2351 $post_sanitize = array(
2352 'subsource' => FILTER_SANITIZE_SPECIAL_CHARS,
2353 'action' => FILTER_SANITIZE_SPECIAL_CHARS,
2354 'pdb_data_keys' => FILTER_SANITIZE_SPECIAL_CHARS,
2355 'submit_button' => FILTER_SANITIZE_SPECIAL_CHARS,
2356 'filename' => FILTER_SANITIZE_SPECIAL_CHARS,
2357 'base_filename' => FILTER_SANITIZE_SPECIAL_CHARS,
2358 'CSV_type' => FILTER_SANITIZE_SPECIAL_CHARS,
2359 'include_csv_titles' => FILTER_VALIDATE_BOOLEAN,
2360 'nocookie' => FILTER_VALIDATE_BOOLEAN,
2361 'previous_multipage' => FILTER_SANITIZE_SPECIAL_CHARS,
2362 'export_selection' => FILTER_SANITIZE_SPECIAL_CHARS,
2363 'pdb_modified' => FILTER_VALIDATE_INT,
2364 );
2365 /*
2366 * $post_input is used for control functions, not for the dataset
2367 */
2368 $post_input = filter_input_array( INPUT_POST, $post_sanitize );
2369
2370 // only process POST arrays from this plugin's pages
2371 if ( empty( $post_input['subsource'] ) || $post_input['subsource'] != self::PLUGIN_NAME or empty( $post_input['action'] ) )
2372 return;
2373
2374 // add a filter to check the submission before anything is done with it
2375 if ( self::apply_filters( 'check_submission', true ) === false )
2376 return;
2377
2378 /*
2379 * the originating page for a multipage form is saved in a session value
2380 *
2381 * if this is an empty string, it is assumed the submission was not part of a multipage form series
2382 */
2383 self::$session->set( 'previous_multipage', $post_input['previous_multipage'] );
2384
2385 /*
2386 * get the defined columns for the submitting shortcode (if any)
2387 *
2388 * this is needed so that validation will be performed on the expected list
2389 * of fields, not just what's found in the POST array
2390 */
2391 $columns = false;
2392 if ( !empty( $post_input['pdb_data_keys'] ) ) {
2393 $columns = self::get_data_key_columns( $post_input['pdb_data_keys'] );
2394 }
2395
2396 /*
2397 * instantiate the validation object if we need to. This is necessary
2398 * because another script can instantiate the object in order to add a
2399 * feedback message
2400 *
2401 * we don't validate administrators in the admin
2402 */
2403 if ( !is_object( self::$validation_errors ) ) {
2404 if ( Participants_Db::is_form_validated() ) {
2405 self::$validation_errors = new PDb_FormValidation();
2406 }
2407 }
2408
2409 switch ( $post_input['action'] ) :
2410
2411 case 'update':
2412 case 'insert':
2413
2414 // validate the request
2415 if ( ! self::submission_is_secure( $post_input['action'] ) )
2416 {
2417 wp_die( 'invalid request', 400 );
2418 }
2419
2420 /*
2421 * we are here for one of these cases:
2422 * a) we're adding a new record in the admin
2423 * b) a user is updating their record on the frontend
2424 * c) an admin is updating a record
2425 *
2426 * signups are processed in the case 'signup' section
2427 *
2428 * set the raw post array filters. We pass in the $_POST array, expecting
2429 * a possibly altered copy of it to be returned
2430 *
2431 * filter: pdb-before_submit_update
2432 * filter: pdb-before_submit_add
2433 */
2434 $post_data = self::apply_filters( 'before_submit_' . ($post_input['action'] === 'insert' ? 'add' : 'update'), $_POST );
2435
2436 if ( isset( $_POST['id'] ) )
2437 {
2438 $record_id = filter_input( INPUT_POST, 'id', FILTER_VALIDATE_INT, array('options' => array('min_range' => 1)) );
2439 } elseif ( isset( $_GET['id'] ) )
2440 {
2441 $record_id = filter_input( INPUT_GET, 'id', FILTER_VALIDATE_INT, array('options' => array('min_range' => 1)) );
2442 } else
2443 {
2444 $record_id = false;
2445 }
2446
2447 $submit_key = array_search( $post_input['submit_button'], self::$i18n );
2448
2449 $modified = isset( $post_input['pdb_modified'] ) && $post_input['pdb_modified'] == 1;
2450
2451 if ( in_array( $submit_key, ['previous','next'] ) && ! $modified )
2452 {
2453 $participant_id = $record_id;
2454 goto redirect; // skip processing the submission
2455 }
2456
2457 $participant_id = self::process_form( $post_data, $post_input['action'], $record_id, $columns, false, 'page submission ' . $post_input['action'] );
2458
2459 if ( false === $participant_id ) {
2460
2461 /**
2462 * @action pdb-submission_not_verified
2463 * @param array submission data
2464 * @param int|bool record id or bool false
2465 * @param PDb_Form_Validation object
2466 */
2467 do_action('pdb-submission_not_verified', $post_data, $record_id, self::$validation_errors );
2468 // we have errors; go back to form and show errors
2469 return;
2470 }
2471
2472 $record = self::get_participant( $participant_id );
2473
2474 /*
2475 * set the stored record hook.
2476 *
2477 * hook: pdb-after_submit_update
2478 * hook: pdb-after_submit_add
2479 */
2480 $wp_hook = self::$prefix . 'after_submit_' . ($post_input['action'] == 'insert' ? 'add' : 'update');
2481 do_action( $wp_hook, $record );
2482
2483 /*
2484 * if we are submitting from the frontend, set the feedback message and
2485 * send the update notification
2486 */
2487 if ( !is_admin() ) {
2488
2489 $feedback_props = array(
2490 "send_notification" => Participants_Db::plugin_setting( 'send_signup_notify_email' ),
2491 "notify_recipients" => Participants_Db::plugin_setting( 'email_signup_notify_addresses' ),
2492 "notify_subject" => Participants_Db::plugin_setting( 'record_update_email_subject' ),
2493 "notify_body" => Participants_Db::plugin_setting( 'record_update_email_body' ),
2494 "email_header" => Participants_Db::$email_headers,
2495 "thanks_message" => self::plugin_setting('record_updated_message'),
2496 'participant_values' => $record,
2497 );
2498
2499 $feedback = new \PDb_submission\feedback($feedback_props);
2500
2501 do_action( 'pdb-before_update_thanks', $feedback );
2502
2503 /*
2504 * if the user is an admin, the validation object won't be instantiated,
2505 * so we do that here so the feedback message can be shown.
2506 */
2507 if ( !is_object( self::$validation_errors ) )
2508 self::$validation_errors = new PDb_FormValidation();
2509
2510 self::$validation_errors->add_error( '', $feedback->thanks_message );
2511
2512 if ( self::plugin_setting_is_true('send_record_update_notify_email') && !self::is_multipage_form() ) {
2513
2514 PDb_Template_Email::send( array(
2515 'to' => $feedback->notify_recipients,
2516 'subject' => $feedback->notify_subject,
2517 'template' => $feedback->notify_body,
2518 'context' => 'record update notify',
2519 ), $feedback->participant_values
2520 );
2521 }
2522
2523 /*
2524 * if the "thanks page" is defined as another page, save the ID in a session variable and move to that page.
2525 */
2526 if ( isset( $post_data['thanks_page'] ) && $post_data['thanks_page'] != $_SERVER['REQUEST_URI'] )
2527 {
2528 self::$session->set( 'pdbid', $post_data['id'] );
2529 self::$session->set( 'previous_multipage', $post_data['shortcode_page'] );
2530
2531 $query_args = apply_filters( 'pdb-record_id_in_get_var', false ) ? array(
2532 PDb_Session::id_var => Participants_Db::$session->session_id(),
2533 //self::$record_query => $record['private_id'],
2534 ) : array();
2535
2536 $redirect = $post_data['thanks_page'];
2537 /**
2538 * this is to handle the special case where the frontend record form uses a separate
2539 * thanks page using the [pdb_signup_thanks] shortcode
2540 */
2541 if ( $post_input['action'] == 'insert' && !self::is_multipage_form() ) {
2542 $query_args = array_merge( $query_args, array( 'action' => 'update' ) );
2543 //self::add_uri_conjunction( $redirect ) . 'action=update';
2544 }
2545
2546 wp_redirect( add_query_arg( $query_args, $redirect ) );
2547
2548 exit;
2549 }
2550
2551 return;
2552 }
2553
2554 redirect:
2555 // redirect according to which submit button was used
2556 switch ( $submit_key )
2557 {
2558 case'apply' :
2559 $redirect = get_admin_url() . 'admin.php?page=' . self::PLUGIN_NAME . '-edit_participant&id=' . $participant_id;
2560 break;
2561
2562 case 'next' :
2563 case 'new' :
2564 $get_id = $post_input['action'] == 'update' ? '&id=' . self::next_id( $participant_id ) : '';
2565 $redirect = get_admin_url() . 'admin.php?page=' . self::PLUGIN_NAME . '-edit_participant' . $get_id;
2566 break;
2567
2568 case 'previous' :
2569 $get_id = $post_input['action'] == 'update' ? '&id=' . self::next_id( $participant_id, false ) : '';
2570 $redirect = get_admin_url() . 'admin.php?page=' . self::PLUGIN_NAME . '-edit_participant' . $get_id;
2571 break;
2572
2573 case 'submit' :
2574 default :
2575 $redirect = get_admin_url() . 'admin.php?page=' . self::PLUGIN_NAME;
2576 \PDb_admin_list\record_list_cache::clear();
2577 }
2578
2579 /**
2580 * @filter pdb-admin_record_edit_submit_redirect
2581 * @param string redirect URL
2582 * @param string submit button key
2583 * @return string redirect URL
2584 */
2585 wp_safe_redirect( Participants_Db::apply_filters( 'admin_record_edit_submit_redirect', $redirect, $submit_key ) );
2586 exit;
2587
2588 case 'output CSV':
2589
2590 if ( ! self::csv_export_allowed() ) {
2591 die();
2592 }
2593
2594 $header_row = array();
2595 $title_row = array();
2596 $data = array();
2597 $filename = !empty( $post_input['filename'] ) ? $post_input['filename'] : '';
2598
2599 switch ( $post_input['CSV_type'] ) :
2600
2601 // create a blank data array
2602 case 'blank':
2603
2604 // add the header row
2605 foreach ( self::get_column_atts( 'CSV' ) as $column )
2606 $header_row[] = $column->name;
2607 $data[] = $header_row;
2608
2609 $i = 2; // number of blank rows to create
2610
2611 while ( $i > 0 ) {
2612 $data[] = array_fill_keys( $header_row, '' );
2613 $i--;
2614 }
2615 break;
2616
2617 case 'participant list':
2618
2619 global $wpdb;
2620
2621 // gets the export field list from the session value or the default set for CSV exports
2622 $csv_columns = self::get_column_atts( self::$session->getArray( 'csv_export_fields', 'CSV' ) );
2623
2624 $export_columns = array();
2625
2626 foreach ( $csv_columns as $column ) {
2627 $field = self::$fields[$column->name];
2628 /* @var $field PDb_Form_Field_Def */
2629 $export_columns[] = sprintf( 'p.%s', $column->name );
2630 $header_row[] = $column->name;
2631 $title_row[] = $field->title();
2632 }
2633
2634 $export_column_list = implode( ', ', $export_columns );
2635
2636 $data['header'] = $header_row;
2637
2638 if ( $post_input['include_csv_titles'] ) {
2639 $data['titles'] = $title_row;
2640 }
2641
2642 $query = false;
2643
2644 $selection_list = array();
2645 if ( isset( $post_input['export_selection'] ) && ! empty( $post_input['export_selection'] ) ) {
2646 $raw_list = explode( ',', str_replace( ' ', '', $post_input['export_selection'] ) );
2647 $selection_list = array_filter( $raw_list, 'is_numeric' );
2648 }
2649
2650 if ( count( $selection_list ) > 0 ) {
2651
2652 $query = 'SELECT ' . $export_column_list . ' FROM ' . Participants_Db::participants_table() . ' p WHERE p.id IN ("' . implode( '","', $selection_list ) . '")';
2653
2654 if ( is_admin() ) {
2655 \PDb_List_Admin::set_admin_user_setting( 'with_selected', 'export' );
2656 }
2657
2658 } elseif ( is_admin() ) {
2659
2660 global $current_user;
2661 $saved_query = self::$session->get( Participants_Db::$prefix . 'admin_list_query-' . $current_user->ID, PDb_List_Admin::default_query() );
2662 $query = str_replace( '*', ' ' . $export_column_list . ' ', $saved_query );
2663
2664 } else {
2665
2666 $saved_query = self::$session->get('csv_export_query');
2667 if ( $saved_query ) {
2668 $query = preg_replace( '#SELECT.+?FROM #', 'SELECT ' . $export_column_list . ' FROM ', $saved_query );
2669 }
2670 }
2671
2672 if ( $query ) {
2673
2674 /**
2675 * @filter pdb-csv_export_query
2676 * @param string $query
2677 * @return string query
2678 */
2679 $query = self::apply_filters('csv_export_query', $query );
2680
2681 $result = $wpdb->get_results( $query, ARRAY_A );
2682
2683 if ( empty( $result ) ) {
2684
2685 Participants_Db::set_admin_message('CSV export failed with error:<pre>' . $wpdb->last_error . '</pre>', 'error' );
2686 $filename = '';
2687
2688 } else {
2689
2690 $data += self::_prepare_CSV_rows( $result );
2691 }
2692
2693 if ( PDB_DEBUG ) {
2694 Participants_Db::debug_log(' CSV export query: '.$query );
2695 }
2696 }
2697
2698 break;
2699
2700 endswitch; // CSV type
2701
2702 if ( !empty( $filename ) ) {
2703
2704 $base_filename = substr( $filename, 0, strpos( $filename, PDb_List_Admin::filename_datestamp() . '.csv' ) );
2705 PDb_List_Admin::set_admin_user_setting( 'csv_base_filename', $base_filename );
2706
2707 // create a file pointer connected to the output stream
2708 $output = fopen( 'php://output', 'w' );
2709
2710 //header('Content-type: application/csv'); // some sources say it should be this
2711 header( 'Content-Type: text/csv; charset=utf-8' );
2712 header( "Cache-Control: no-store, no-cache" );
2713 header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
2714
2715 // output the data lines
2716 /**
2717 * @filter pdb-csv_output_data
2718 * @param array sanitized CSV output data array of data lines
2719 * @return array
2720 */
2721 foreach ( self::apply_filters( 'csv_output_data', $data ) as $line ) {
2722 fputcsv( $output, $line, PDb_admin_list\csv::delimiter(), PDb_admin_list\csv::enclosure() );
2723 }
2724
2725 fclose( $output );
2726
2727 // we must terminate the script to prevent additional output being added to the CSV file
2728 exit;
2729 }
2730
2731 return; // $data;
2732
2733 case 'retrieve' :
2734
2735 if ( self::nonce_check( filter_input( INPUT_POST, 'session_hash', FILTER_SANITIZE_SPECIAL_CHARS ), self::$main_submission_nonce_key . $post_input['action'] ) ) {
2736 self::_process_retrieval();
2737 }
2738 return;
2739
2740 case 'signup' :
2741
2742 if ( !self::nonce_check( filter_input( INPUT_POST, 'session_hash', FILTER_SANITIZE_SPECIAL_CHARS ), self::$main_submission_nonce_key . $post_input['action'] ) )
2743 return;
2744
2745 $_POST['private_id'] = '';
2746 $columns[] = 'private_id';
2747
2748 /*
2749 * route the $_POST data through a callback if defined
2750 *
2751 * filter: pdb-before_submit_signup
2752 */
2753 $post_data = self::apply_filters( 'before_submit_signup', $_POST );
2754
2755 // sets the blank id for the new record
2756 $post_data['id'] = 0;
2757
2758 /*
2759 * the signup form should update the current record if it is revisited during a multipage form session
2760 */
2761 $submit_action = 'insert';
2762 if ( self::$session->record_id() !== false ) {
2763 $submit_action = 'update';
2764 }
2765
2766 // submit the data
2767 $post_data['id'] = self::process_form( $post_data, $submit_action, self::$session->record_id(), $columns, false, 'signup form submission' );
2768
2769 if ( false !== $post_data['id'] ) {
2770
2771 $record = self::get_participant( $post_data['id'] );
2772 /*
2773 * hook: pdb-after_submit_signup
2774 */
2775 $wp_hook = self::$prefix . 'after_submit_signup';
2776 do_action( $wp_hook, $record );
2777
2778 $redirect = $post_data['thanks_page'];
2779 if ( Participants_Db::plugin_setting_is_true( 'use_cache_buster' ) || Participants_Db::plugin_setting_is_true( 'use_session_alternate_method' ) ) {
2780 $redirect = PDb_Session::session_url_var($post_data['thanks_page']);
2781 }
2782
2783 if ( apply_filters( 'pdb-record_id_in_get_var', false ) ) {
2784 $redirect = add_query_arg( array(
2785 PDb_Session::id_var => self::$session->session_id(),
2786 //self::$record_query => $record['private_id'],
2787 ), $post_data['thanks_page'] );
2788 }
2789
2790 self::$session->set( 'pdbid', $post_data['id'] );
2791 self::$session->set( 'previous_multipage', $post_data['shortcode_page'] );
2792 self::$session->set( 'form_status', array_key_exists( 'previous_multipage', $_POST ) ? 'multipage-signup' : 'normal' );
2793
2794 wp_redirect( $redirect );
2795
2796 exit;
2797 } else {
2798
2799 /**
2800 * @action pdb-signup_submission_not_verified
2801 * @param array submission data
2802 * @param int|bool record id or bool false
2803 * @param PDb_Form_Validation object
2804 */
2805 do_action('pdb-signup_submission_not_verified', $post_data, $post_data['id'], self::$validation_errors );
2806 }
2807
2808 return;
2809
2810 endswitch; // $_POST['action']
2811 }
2812
2813 /**
2814 * validates an update or insert submission
2815 *
2816 * this is used for one of these three cases:
2817 * adding a new record in the admin
2818 * updating a record in the admin
2819 * a unauthenticated user is updating their record on the frontend
2820 *
2821 * @param string $action the current submission action key
2822 * @return bool true if the submission can be accepted
2823 */
2824 private static function submission_is_secure( $action )
2825 {
2826 $role_check = false;
2827
2828 if ( Participants_Db::is_admin() )
2829 {
2830 $context = 'update/insert submission check';
2831 $role_check = self::current_user_has_plugin_role( 'editor', $context ) || self::current_user_has_plugin_role( 'admin', $context );
2832 }
2833 else if ( $action === 'update' ) // the unauthenticated user
2834 {
2835 // make sure the private ID matches the submitted record id
2836 $id_check = filter_input( INPUT_POST, 'id', FILTER_SANITIZE_NUMBER_INT ) === self::get_participant_id( filter_input( INPUT_POST, Participants_Db::$record_query, FILTER_SANITIZE_SPECIAL_CHARS ) );
2837 $role_check = $id_check;
2838 $action = 'record'; // this is the action string used by the frontend record update form
2839 }
2840
2841 $nonce_check = self::nonce_check( filter_input( INPUT_POST, 'session_hash', FILTER_SANITIZE_SPECIAL_CHARS ), self::$main_submission_nonce_key . $action );
2842
2843 return $nonce_check && $role_check;
2844 }
2845
2846 /**
2847 * checks the nonce on a form submission
2848 *
2849 * @uses filter pdb-nonce_verify
2850 *
2851 * @param string $nonce the nonce value
2852 * @param string $context the context string
2853 *
2854 * @return bool true if nonce passes
2855 */
2856 public static function nonce_check( $nonce, $context )
2857 {
2858 return self::apply_filters( 'nonce_verify', wp_verify_nonce( $nonce, self::nonce_key( $context ) ), $context );
2859 }
2860
2861 /**
2862 * provides a key string for a nonce
2863 *
2864 * @uses filter pdb-nonce_key
2865 *
2866 * @param string $context a context string for the key string
2867 *
2868 * @return string
2869 */
2870 public static function nonce_key( $context )
2871 {
2872 return self::apply_filters( 'nonce_key', self::$prefix . $context );
2873 }
2874
2875 /**
2876 * provides a nonce string
2877 *
2878 * @uses filter pdb-nonce
2879 *
2880 * @param string $context an optional context string for the filter
2881 *
2882 * @return string
2883 */
2884 public static function nonce( $context )
2885 {
2886 return self::apply_filters( 'nonce', wp_create_nonce( self::nonce_key( $context ) ) );
2887 }
2888
2889 /**
2890 * tests a private link retrieval submission and send the link or sets an error
2891 *
2892 * @return null
2893 */
2894 private static function _process_retrieval()
2895 {
2896 /**
2897 * @filter pdb-retrieve_link_brute_force_protect
2898 * @param bool default setting
2899 * @return bool false to bypass brute force check
2900 */
2901 if ( self::apply_filters( 'retrieve_link_brute_force_protect', true ) ) :
2902 /*
2903 * we check a transient based on the user's IP; if the user tries more than 3
2904 * times per day to get a private ID, they are blocked for 24 hours
2905 */
2906 $max_tries = Participants_Db::current_user_has_plugin_role( 'admin', 'retrieve link' ) ? 10000 : 3; // give the plugin admin unlimited tries
2907 $transient = self::$prefix . 'retrieve-count-' . str_replace( '.', '', self::user_ip() );
2908 $count = get_transient( $transient ) ? : 0; // set the count to 0 if no transient is set
2909 if ( $count > $max_tries ) {
2910
2911 // too many tries, come back tomorrow
2912 self::debug_log( 'Participants Database Plugin: IP blocked for too many retrieval attempts from IP ' . self::user_ip() . ' in 24-hour period.' );
2913 return;
2914 }
2915 $count++;
2916 set_transient( $transient, $count, DAY_IN_SECONDS );
2917
2918 endif; // brute force protect filter
2919
2920 $column = self::plugin_setting( 'retrieve_link_identifier', 'email' );
2921
2922 if ( !is_object( self::$validation_errors ) ) {
2923 self::$validation_errors = new PDb_FormValidation();
2924 }
2925
2926 if ( self::plugin_setting_is_true( 'retrieve_form_captcha' ) )
2927 {
2928 $captchafield = PDb_Shortcode::get_captcha_fieldname();
2929 if ( ! empty( $captchafield ) )
2930 {
2931 $captchavalue = isset( $_POST[$captchafield] ) ? $_POST[$captchafield] : '';
2932 self::$validation_errors->validate_field( $captchavalue, $captchafield, 'captcha');
2933
2934 if ( self::$validation_errors->has_error( $captchafield ) )
2935 {
2936 return;
2937 }
2938 }
2939 }
2940
2941 if ( !isset( $_POST[$column] ) || empty( $_POST[$column] ) ) {
2942 self::$validation_errors->add_error( $column, 'empty' );
2943 return;
2944 }
2945
2946 // a value was submitted, try to find a record with it
2947 $match_id = self::find_record_match( $column, $_POST );
2948
2949 if ( is_numeric( $match_id ) ) {
2950 $participant_values = self::get_participant( $match_id );
2951 } else {
2952 self::$validation_errors->add_error( $column, 'identifier' );
2953 return;
2954 }
2955 // prepare an object for the filter to use
2956 $retrieve_link_email = new stdClass();
2957 $retrieve_link_email->body_template = self::plugin_setting( 'retrieve_link_email_body' );
2958 $retrieve_link_email->subject = self::plugin_setting( 'retrieve_link_email_subject' );
2959 $retrieve_link_email->recipient = $participant_values[self::plugin_setting( 'primary_email_address_field', 'email' )];
2960 /**
2961 * @version 1.6
2962 *
2963 * @action pdb-before_send_retrieve_link_email
2964 * @param object $retrieve_link_email
2965 * @param array $participants_values
2966 */
2967 self::apply_filters( 'before_send_retrieve_link_email', $retrieve_link_email, $participant_values );
2968 if ( !empty( $retrieve_link_email->recipient ) ) {
2969 PDb_Template_Email::send( array(
2970 'to' => $retrieve_link_email->recipient,
2971 'subject' => $retrieve_link_email->subject,
2972 'template' => $retrieve_link_email->body_template,
2973 'context' => 'retrieve link email',
2974 ), $match_id
2975 );
2976 } else {
2977 self::debug_log( __METHOD__ . ' primary email address field undefined' );
2978 }
2979
2980 if ( self::plugin_setting_is_true( 'send_retrieve_link_notify_email' ) ) {
2981
2982 PDb_Template_Email::send( array(
2983 'to' => self::plugin_setting( 'email_signup_notify_addresses' ),
2984 'subject' => self::plugin_setting( 'retrieve_link_notify_subject' ),
2985 'template' => self::plugin_setting( 'retrieve_link_notify_body' ),
2986 'context' => 'retrieve link notification',
2987 ), $match_id
2988 );
2989 }
2990
2991 //self::$validation_errors->add_error('', 'success');
2992 $_POST['action'] = 'success';
2993 return;
2994 }
2995
2996 /**
2997 * processes a rich text string
2998 *
2999 * runs it through the WP the_content filter if selected in the settings
3000 *
3001 * @param string $input
3002 * @param string $context a context-identifying string
3003 * @return string
3004 */
3005 public static function process_rich_text( $string, $context = '' )
3006 {
3007 /**
3008 * @version 1.6.3
3009 * @filter pdb-rich_text_auto_formatting
3010 * @param string $string the raw rich text
3011 * @param string $context a context identifier for the filter
3012 * @return string
3013 */
3014 $string = self::apply_filters( 'rich_text_auto_formatting', $string, $context );
3015 /**
3016 * @filter pdb-rich_text_filter_mode
3017 * @param string current filter mode setting
3018 * @param string the content to be processed
3019 * @return string filter mode to use
3020 */
3021 $filter_mode = self::apply_filters( 'rich_text_filter_mode', Participants_Db::plugin_setting('enable_wpautop', 'the_content'), $string );
3022
3023 switch ( $filter_mode ) {
3024 case '1':
3025 case 'the_content':
3026 return self::rich_text_filter( $string );
3027 case 'wpautop':
3028 return wpautop($string);
3029 case 'wpautop+shortcodes':
3030 return do_shortcode( wpautop($string) );
3031 case '0':
3032 case 'none':
3033 default:
3034 return $string;
3035 }
3036 }
3037
3038 /**
3039 * applies the rich text filter
3040 *
3041 * this can be overridden; the filter is set in self::init()
3042 *
3043 * @param string $string
3044 * @return string
3045 */
3046 public static function rich_text_filter( $string )
3047 {
3048 /**
3049 * @filter pdb-rich_text_filter
3050 * @param string name of the filter to apply to rich text
3051 * @since 1.7.1.4
3052 */
3053 return apply_filters( self::apply_filters( 'rich_text_filter', 'the_content' ), $string );
3054 }
3055
3056 /**
3057 * gets an array of readonly fields
3058 *
3059 * @return array
3060 */
3061 public static function get_readonly_fields()
3062 {
3063
3064 $fields = array();
3065
3066 foreach ( self::get_column_atts( 'readonly' ) as $column )
3067 $fields[] = $column->name;
3068
3069 return $fields;
3070 }
3071
3072 /**
3073 * returns the title attribute of a column
3074 *
3075 * @param string $column field name
3076 * @return string
3077 */
3078 public static function column_title( $column )
3079 {
3080 $field = self::get_column( $column );
3081
3082 return is_object($field) ? $field->title() : $column;
3083 }
3084
3085 /**
3086 * prepares a set of rows for CSV output
3087 *
3088 * @param array $raw_array the raw array output from the query
3089 *
3090 * @return array of record arrays
3091 */
3092 private static function _prepare_CSV_rows( $raw_array )
3093 {
3094 $output = array();
3095
3096 foreach ( $raw_array as $value ) {
3097 $output[] = self::_prepare_CSV_row( $value );
3098 }
3099
3100 return $output;
3101 }
3102
3103 /**
3104 * prepares a row of data for CSV output
3105 *
3106 * @param array $raw_array the raw array output from the query
3107 *
3108 * @return array with all the serialized arrays in human-readable form
3109 */
3110 private static function _prepare_CSV_row( $raw_array )
3111 {
3112
3113 $output = array();
3114
3115 foreach ( $raw_array as $key => $raw_value ) {
3116
3117 $field = new PDb_Field_Item( $key );
3118
3119 /**
3120 * filters the raw value of the field before exporting
3121 *
3122 * @version 1.7.1
3123 * @filter pdb-csv_export_value_raw
3124 * @param mixed the raw value
3125 * @param PDb_Field_Item the field object
3126 * @return mixed
3127 */
3128 $field->set_value( self::apply_filters( 'csv_export_value_raw', $raw_value, $field ) );
3129
3130 /*
3131 * decode HTML entities and convert line breaks to <br>, then pass to a filter
3132 * for processing before being added to the output array
3133 */
3134 /**
3135 * @filter pdb-csv_export_value
3136 *
3137 * provides a way to make a final tweak to the export value
3138 *
3139 * return bool false to skip the field, removing it from the export
3140 *
3141 * @param string the export value
3142 * @param PDb_Field_Item the field object
3143 * @return string
3144 */
3145 $output_value = Participants_Db::apply_filters( 'csv_export_value', self::unix_linebreaks( $field->export_value() ), $field );
3146
3147 if ( $output_value === false ) {
3148 continue 1;
3149 }
3150
3151 $output[$key] = $output_value;
3152 }
3153
3154 return $output;
3155 }
3156
3157 /**
3158 * converts line breaks to standard unix format
3159 *
3160 * @param string $s
3161 * @return string
3162 */
3163 public static function unix_linebreaks( $s )
3164 {
3165 $s = str_replace("\r\n", "\n", $s);
3166 $s = str_replace("\r", "\n", $s);
3167 $s = preg_replace("/\n{2,}/", "\n\n", $s);
3168 return $s;
3169 }
3170
3171 /**
3172 * creates an anchor element with clickable link and href
3173 *
3174 * this is simply an interface to the xnau_FormElement function of the same name
3175 *
3176 * @static
3177 * @param string $link the URI
3178 * @param string $linktext the clickable text (optional)
3179 * @param string $template the format of the link (optional)
3180 * @param array $get an array of name=>value pairs to include in the get string
3181 *
3182 * @return string HTML or HTML-escaped string (if it's not a link)
3183 */
3184 public static function make_link( $link, $linktext = '', $template = false, $get = false )
3185 {
3186
3187 $field = new stdClass();
3188
3189 $field->link = $link;
3190 $field->value = $linktext === '' ? $link : $linktext;
3191
3192 return PDb_FormElement::make_link( $field, $template, $get );
3193 }
3194
3195 /**
3196 * provides an AJAX loading spinner element
3197 */
3198 public static function get_loading_spinner()
3199 {
3200 $bitmap_source = plugins_url( 'ui/ajax-loader.gif', __FILE__ );
3201 $svg_source = plugins_url( 'ui/ajax-loader.svg', __FILE__ );
3202 $index = isset($_SERVER['HTTP_USER_AGENT']) && stripos( $_SERVER['HTTP_USER_AGENT'], 'safari' ) !== false ? 0 : 1;
3203 $template = array(
3204 '<span class="ajax-loading"><object data="%1$s"><img src="%2$s" /></object></span>',
3205 '<svg class="ajax-loading" ><image xlink:href="%1$s" src="%2$s" /></svg>'
3206 );
3207 /**
3208 * @version 1.6.3
3209 * @filter pdb-loading_spinner_html
3210 * @param string html
3211 * @return string
3212 */
3213 return self::apply_filters( 'loading_spinner_html', sprintf( $template[$index], $svg_source, $bitmap_source ) );
3214 }
3215
3216
3217 /**
3218 * attempt to create the uploads directory
3219 *
3220 * sets an error if it fails
3221 *
3222 * @param string $dir the name of the new directory
3223 * @return bool success
3224 */
3225 public static function _make_uploads_dir( $dir = '' )
3226 {
3227 $dir = empty( $dir ) ? Participants_Db::files_location() : $dir;
3228 $savedmask = umask( 0 );
3229
3230 // create the uploads directory
3231 $status = mkdir( Participants_Db::base_files_path() . $dir, 0755, true );
3232
3233 if ( $status === false )
3234 {
3235 $message = sprintf( __( 'The uploads directory (%s) could not be created.', 'participants-database' ), $dir ) . '<a href="https://xnau.com/work/wordpress-plugins/participants-database/participants-database-documentation/participants-database-settings-help/#File-Upload-Location"><span class="dashicons dashicons-editor-help"></span></a>';
3236
3237 if ( is_object( self::$validation_errors ) )
3238 {
3239 self::$validation_errors->add_error( '', $message );
3240 } else {
3241 PDb_Admin_Notices::post_error( $message, '' );
3242 }
3243 }
3244
3245 umask( $savedmask );
3246 return $status;
3247 }
3248
3249 /**
3250 * builds a record edit link
3251 *
3252 * @param string $PID private id value
3253 * @return string private record URI
3254 */
3255 public static function get_record_link( $PID, $target_page = '' )
3256 {
3257 $target_page = $target_page === '' ? self::plugin_setting('registration_page') : $target_page;
3258
3259 $registration_page = self::find_permalink( $target_page );
3260
3261 if ( false === $registration_page ) {
3262 //error_log( 'Participants Database: "Participant Record Page" setting is invalid.' );
3263 return '';
3264 }
3265
3266 /**
3267 * @version 1.6.3
3268 * @filter pdb-record_edit_page
3269 */
3270 /**
3271 * @since 1.7.1.1
3272 * @filter pdb-record_edit_url
3273 * @param string the full URL to the record edit page with the query var
3274 * @param string the private ID value
3275 */
3276 return self::apply_filters( 'record_edit_url', self::add_uri_conjunction( self::apply_filters( 'record_edit_page', $registration_page ) ) . Participants_Db::$record_query . '=' . $PID, $PID );
3277 }
3278
3279 /**
3280 * builds an admin record edit link
3281 *
3282 * this is meant to be included in the admin notification for a new signup,
3283 * giving them the ability to click the link and edit the new record
3284 *
3285 * @param int $id the id of the new record
3286 * @return string the HREF for the record edit link
3287 */
3288 public static function get_admin_record_link( $id )
3289 {
3290 $path = 'admin.php?page=participants-database-edit_participant&action=edit&id=' . $id;
3291
3292 return get_admin_url( NULL, $path );
3293 }
3294
3295 /**
3296 * prints the list with filtering parameters applied
3297 *
3298 * called by the wp_ajax_nopriv_pdb_list_filter action: this happens when a
3299 * user submits a search or sort on a record list
3300 *
3301 * the POSTed search values are incorporated in PDb_List_Query::_add_filter_from_post()
3302 *
3303 * @return null
3304 */
3305 public static function pdb_list_filter()
3306 {
3307 $multi = isset( $_POST['search_field'] ) && is_array( $_POST['search_field'] );
3308 $postinput = filter_input_array( INPUT_POST, self::search_post_filter( $multi ) );
3309
3310 self::$instance_index = empty( $postinput['target_instance'] ) ? $postinput['instance_index'] : $postinput['target_instance'];
3311
3312 global $post;
3313
3314 if ( !is_object( $post ) ) {
3315 $post = get_post( $postinput['postID'] );
3316 }
3317
3318 self::print_list_search_result( $post, self::$instance_index );
3319
3320 do_action( Participants_Db::$prefix . 'list_ajax_complete', $post );
3321
3322 exit;
3323 }
3324
3325 /**
3326 * supplies the inline CSS for the frontend
3327 *
3328 * this is where user preferences are added to the CSS
3329 *
3330 * @return string
3331 */
3332 private static function inline_css()
3333 {
3334 return '
3335 .image-field-wrap img {
3336 height:' . self::css_dimension_value( self::plugin_setting( 'default_image_size', '3em' ) ) . ';
3337 max-width: inherit;
3338 }
3339 .pdb-list .image-field-wrap img {
3340 height:' . self::css_dimension_value( self::plugin_setting( 'list_default_image_size', '50px' ) ) . ';
3341 max-width: inherit;
3342 }
3343 '. self::inline_css_custom_props();
3344 }
3345
3346 /**
3347 * prints a custom CSS property value declaration
3348 *
3349 * @param array $property_values
3350 */
3351 private static function inline_css_custom_props()
3352 {
3353 $inline = '';
3354 $p_selector = "%s {\n%s\n}\n";
3355 $p_rule = ' --PDb-%s: %s; ';
3356 $css = [];
3357 $rule_list = [];
3358
3359 foreach( self::css_custom_props() as $propname => $value )
3360 {
3361 if ( $value )
3362 {
3363 $rule_list[] = sprintf( $p_rule, $propname, $value );
3364 }
3365 }
3366
3367 if ( ! empty( $rule_list ) )
3368 {
3369 $css[] = sprintf( $p_selector, ':root', implode( PHP_EOL, $rule_list ) );
3370 }
3371
3372 if ( ! empty( $css ) )
3373 {
3374 $inline = implode( PHP_EOL, $css );
3375 }
3376
3377 return $inline;
3378 }
3379
3380 /**
3381 * provides an array of custom CSS property values
3382 *
3383 * @return array as $propname => $value
3384 */
3385 private static function css_custom_props()
3386 {
3387 $custom_props = [];
3388
3389 $color_mode = [
3390 'default' => [
3391 'pagination-border-color' => 'rgba(204, 204, 204, 1)',
3392 'pagination-hover-color' => '#CCC',
3393 'pagination-bg'=> '#FAFAFA',
3394 'pagination-current-bg' => 'rgba(204, 204, 204, 1)',
3395 'pagination-current-color' => '#FFF',
3396 'pagination-disabled-bg' => '#F3F3F3',
3397 'pagination-disabled-color' => '#777',
3398 'message-bg' => '#FFF',
3399 'message-shadow' => '0 1px 1px 0 rgba(0, 0, 0, 0.1)',
3400 'flex-row-bg' => 'rgba(0,0,0,0.05)',
3401 ],
3402 'dark' => [
3403 'pagination-border-color' => 'rgba(255,255,255,0.2)',
3404 'pagination-hover-color' => 'rgba(255,255,255,0.5)',
3405 'pagination-bg'=> 'rgba( 255,255,255,0.1)',
3406 'pagination-current-bg' => 'rgba(255, 255, 255, 0.5)',
3407 'pagination-current-color' => false,
3408 'pagination-disabled-bg' => false,
3409 'pagination-disabled-color' => 'rgba(255,255,255,0.4)',
3410 'message-bg' => 'rgba(255,255,255,0.3)',
3411 'message-shadow' => '1px 1px 3px 0 rgba(0, 0, 0, 0.3)',
3412 'flex-row-bg' => 'rgba(255,255,255,0.05)',
3413 ],
3414 'light' => [
3415 'pagination-border-color' => 'rgba(0,0,0,0.2)',
3416 'pagination-hover-color' => 'rgba(0,0,0,0.5)',
3417 'pagination-bg' => 'rgba( 0,0,0,0.1)',
3418 'pagination-current-bg' => 'rgba(0,0,0, 0.2)',
3419 'pagination-current-color' => false,
3420 'pagination-disabled-bg' => false,
3421 'pagination-disabled-color' => 'rgba(0,0,0,0.2)',
3422 'message-bg' => 'rgba(0,0,0,0.1)',
3423 'message-shadow' => '1px 1px 1px 0 rgba(255, 255, 255, 0.3)',
3424 'flex-row-bg' => 'rgba(0,0,0,0.05)',
3425 ]
3426 ];
3427
3428 $color_mode_setting = is_admin() ? 'default' : self::plugin_setting('color_mode', 'default');
3429
3430 $custom_props += $color_mode[ $color_mode_setting ];
3431
3432 return self::apply_filters('css_custom_properties', $custom_props );
3433 }
3434 /**
3435 * provides the list output from an AJAX search
3436 *
3437 * @param object $post the current post object
3438 * @param int $instance the instance index of the targeted list
3439 * @return null
3440 */
3441 private static function print_list_search_result( $post, $instance )
3442 {
3443 /*
3444 * get the attributes array; these values were saved in the session array by
3445 * the Shortcode class when it was instantiated
3446 */
3447 $session = self::$session->getArray( 'shortcode_atts' );
3448
3449 $shortcode_atts = isset( $session[$post->ID]['list'][$instance] ) ? $session[$post->ID]['list'][$instance] : false;
3450
3451 if ( ! is_array( $shortcode_atts ) ) {
3452 printf( 'failed to get session for list instance %s', $instance );
3453 return;
3454 }
3455
3456 if ( isset( $shortcode_atts['header_sort'] ) && $shortcode_atts['header_sort'] === 'true' )
3457 {
3458 new PDb_shortcodes\sort_headers();
3459 }
3460
3461 // add the AJAX filtering flag
3462 $shortcode_atts['filtering'] = 1;
3463 $shortcode_atts['module'] = 'list';
3464
3465 // output the filtered shortcode content
3466 echo wp_kses( PDb_List::get_list( $shortcode_atts ), Participants_Db::allowed_html('form') );
3467
3468 return;
3469 }
3470
3471 /**
3472 * clears the list search
3473 *
3474 * @return null
3475 */
3476 private static function clear_list_search()
3477 {
3478 self::$session->clear( 'shortcode_atts' );
3479 }
3480
3481 /**
3482 * supplied for backwards compatibility
3483 *
3484 * the original func has been superceded, but this will allow the old func to be used
3485 *
3486 * @var string $value
3487 * @var string $form_element
3488 * @return string
3489 */
3490 public static function prep_field_for_display( $value, $form_element )
3491 {
3492 $field = (object) array(
3493 'value' => $value,
3494 'form_element' => $form_element,
3495 'module' => 'single', // probably not correct, but this is the generic option
3496 );
3497 return PDb_FormElement::get_field_value_display( $field );
3498 }
3499
3500 /**
3501 * adds a validation arror message
3502 *
3503 * @param string $message
3504 * @param string $fieldname name of the field involved (if any)
3505 * @return null
3506 */
3507 public static function validation_error( $message, $fieldname = '' )
3508 {
3509 self::_show_validation_error($message, $fieldname);
3510 }
3511
3512 /**
3513 * shows a validation error message
3514 *
3515 * @param string $error the message to show
3516 * @param string $name the field on which the error was called
3517 */
3518 private static function _show_validation_error( $error, $name = '', $overwrite = true )
3519 {
3520 if ( is_object( self::$validation_errors ) )
3521 {
3522 self::$validation_errors->add_error( $name, $error, $overwrite );
3523 }
3524 else
3525 {
3526 self::set_admin_message( $error );
3527 }
3528 }
3529
3530 /**
3531 * sets up a few internationalization words
3532 */
3533 private static function _set_i18n()
3534 {
3535 self::$i18n = [
3536 'submit' => __( 'Submit', 'participants-database' ),
3537 'apply' => __( 'Apply', 'participants-database' ),
3538 'next' => __( 'Next', 'participants-database' ),
3539 'new' => __( 'New', 'participants-database' ),
3540 'previous' => __( 'Previous', 'participants-database' ),
3541 'updated' => __( 'The record has been updated.', 'participants-database' ),
3542 'added' => __( 'The new record has been added.', 'participants-database' ),
3543 'zero_rows_error' => __( 'No record was added on query: %s', 'participants-database' ),
3544 'database_error' => __( 'Database Error: %2$s on query: %1$s', 'participants-database' )
3545 ];
3546 }
3547
3548 /**
3549 * sets some custom body classes in the admin
3550 *
3551 * @param string $classes
3552 * @return string
3553 */
3554 public static function add_admin_body_class( $class )
3555 {
3556 $class .= ' pdb-jquery-ui ';
3557 if ( self::has_dashicons() ) {
3558 $class .= ' has-dashicons ';
3559 }
3560 return $class;
3561 }
3562
3563 /**
3564 * sets some custom body classes
3565 *
3566 * @global WP_Post $post
3567 * @param array $classes
3568 */
3569 public static function add_body_class( $classes )
3570 {
3571 if ( self::has_dashicons() ) {
3572 $classes[] = 'has-dashicons';
3573 }
3574 global $post;
3575 $shortcodes = is_object( $post ) ? self::get_plugin_shortcodes( $post->post_content ) : '';
3576 if ( !empty( $shortcodes ) ) {
3577 $classes[] = 'participants-database-shortcode';
3578 foreach ( $shortcodes as $shortcode ) {
3579 $classes[] = $shortcode;
3580 }
3581 }
3582 return $classes;
3583 }
3584
3585 /**
3586 * checks the WP version for the availability of dashicon fonts
3587 *
3588 * @return bool true if the font is available
3589 */
3590 public static function has_dashicons()
3591 {
3592 return version_compare( get_bloginfo( 'version' ), '3.8', '>=' );
3593 }
3594
3595 /**
3596 * prints an admin page heading
3597 *
3598 * @param text $text text to show if not the title of the plugin
3599 */
3600 public static function admin_page_heading( $text = false )
3601 {
3602
3603 $text = $text ? $text : self::$plugin_title;
3604 ?>
3605 <div class="icon32" id="icon-users"></div><h2><?php echo esc_html( $text ) ?></h2>
3606 <?php
3607 self::admin_message();
3608 }
3609
3610 /**
3611 * sets up the plugin admin menus
3612 *
3613 * fired on the admin_menu hook
3614 *
3615 * @return null
3616 */
3617 public static function plugin_menu()
3618 {
3619 if ( filter_input( INPUT_GET, 'page', FILTER_SANITIZE_SPECIAL_CHARS ) === self::$plugin_page . '_settings_page' ) {
3620 /*
3621 * intialize the plugin settings for the plugin settings page
3622 */
3623 self::$Settings->initialize();
3624 }
3625
3626 /*
3627 * this allows the possibility of a child class handling the admin list display
3628 */
3629 $list_admin_classname = self::apply_filters( 'admin_list_classname', 'PDb_List_Admin' );
3630
3631 // define the plugin admin menu pages
3632 add_menu_page(
3633 self::$plugin_title, self::$plugin_title, self::plugin_capability( 'record_edit_capability', 'main admin menu' ), self::PLUGIN_NAME, null
3634 );
3635
3636 // add the list participants page
3637 add_submenu_page(
3638 self::PLUGIN_NAME,
3639 self::plugin_label( 'list_participants' ),
3640 self::plugin_label( 'list_participants' ),
3641 self::plugin_capability( 'record_edit_capability', 'list participants' ),
3642 self::PLUGIN_NAME, //self::$plugin_page . '-list_participants',
3643 array($list_admin_classname, 'initialize')
3644 );
3645 /**
3646 * this registers the edit participant page without adding it as a menu item
3647 *
3648 * had to change how this is done for php 8.2
3649 */
3650 add_submenu_page(
3651 self::$plugin_page . '-add_participant', '', '', self::plugin_capability( 'record_edit_capability', 'edit participant' ), self::$plugin_page . '-edit_participant', array(__CLASS__, 'include_admin_file')
3652 );
3653
3654 add_submenu_page(
3655 self::PLUGIN_NAME, self::plugin_label( 'add_participant' ), self::plugin_label( 'add_participant' ), self::plugin_capability( 'record_edit_capability', 'add participant' ), self::$plugin_page . '-add_participant', array(__CLASS__, 'include_admin_file')
3656 );
3657
3658 add_submenu_page(
3659 self::PLUGIN_NAME, self::plugin_label( 'manage_fields' ), self::plugin_label( 'manage_fields' ), self::plugin_capability( 'plugin_admin_capability', 'manage fields' ), self::$plugin_page . '-manage_fields', array(__CLASS__, 'include_admin_file')
3660 );
3661
3662 add_submenu_page(
3663 self::PLUGIN_NAME, self::plugin_label( 'manage_list_columns' ), self::plugin_label( 'manage_list_columns' ), self::plugin_capability( 'plugin_admin_capability', 'manage fields' ), self::$plugin_page . '-manage_list_columns', array(__CLASS__, 'include_admin_file')
3664 );
3665
3666 add_submenu_page(
3667 self::PLUGIN_NAME, self::plugin_label( 'upload_csv' ), self::plugin_label( 'upload_csv' ), self::plugin_capability( 'plugin_admin_capability', 'upload csv' ), self::$plugin_page . '-upload_csv', array(__CLASS__, 'include_admin_file')
3668 );
3669
3670 add_submenu_page(
3671 self::PLUGIN_NAME, self::plugin_label( 'plugin_settings' ), self::plugin_label( 'plugin_settings' ), self::plugin_capability( 'plugin_admin_capability', 'plugin settings' ), self::$plugin_page . '_settings_page', array(self::$Settings, 'show_settings_form')
3672 );
3673
3674 add_submenu_page(
3675 self::PLUGIN_NAME, self::plugin_label( 'setup_guide' ), self::plugin_label( 'setup_guide' ), self::plugin_capability( 'plugin_admin_capability', 'setup guide' ), self::$plugin_page . '-setup_guide', array(__CLASS__, 'include_admin_file')
3676 );
3677 }
3678
3679 /**
3680 * prints a credit footer for the plugin
3681 *
3682 * @return null
3683 */
3684 public static function plugin_footer()
3685 {
3686 $greeting = false;
3687 if ( self::apply_filters( 'show_live_notifications', true ) ) {
3688 $greeting = PDb_xnau_Greeting::greeting();
3689 }
3690 ?>
3691 <?php if ( $greeting ) : ?>
3692 <div id="PDb_greeting" class="pdb-footer padded widefat postbox pdb-live-notification">
3693 <?php echo wp_kses_post( $greeting ); ?>
3694 </div>
3695 <?php endif; ?>
3696 <?php
3697 /**
3698 * @filter pdb-show_plugin_colophon
3699 * @param bool
3700 * @return bool true if colophon should be shown
3701 */
3702 if ( self::apply_filters( 'show_plugin_colophon', true ) ) :
3703 ob_start();
3704 ?>
3705 <div id="PDb_footer" class="pdb-footer widefat redfade postbox">
3706 <div class="section">
3707 <h4><?php echo 'Participants Database ', self::$plugin_version ?><br /><?php _e( 'WordPress Plugin', 'participants-database' ) ?></h4>
3708 <p><em><?php _e( 'Helping organizations manage their volunteers, members and participants.', 'participants-database' ) ?></em></p>
3709 </div>
3710 <div class="section">
3711 <h4><a class="glyph-link" href="http://xnau.com"><span class="icon-xnau-glyph"></span></a><?php _e( 'Developed by', 'participants-database' ) ?><br /><a href="http://xnau.com">xn<span class="lowast">&lowast;</span>au webdesign</a></h4>
3712 <p><?php _e( 'Suggestions or criticisms of this plugin? I&#39;d love to hear them: email ', 'participants-database' ) ?><a href="mailto:support@xnau.com">support@xnau.com.</a>
3713 </div>
3714 <div class="section">
3715 <p><?php printf( __( 'Please consider contributing to the continued support and development of this software by visiting %1$sthis plugin&#39;s page,%3$s giving the plugin a %2$srating%3$s or review, or dropping something in the %1$stip jar.%3$s Thanks!', 'participants-database' ), '<a href="http://xnau.com/wordpress-plugins/participants-database#donation-link">', '<a href="http://wordpress.org/extend/plugins/participants-database/">', '</a>' ) ?></p>
3716 </div>
3717 </div>
3718 <?php
3719 /**
3720 * @filter pdb-plugin_colophon_html
3721 * @param string HTML
3722 * @return string HTML
3723 */
3724 echo Participants_Db::apply_filters('plugin_colophon_html', ob_get_clean() );
3725 endif;
3726 }
3727
3728 /**
3729 * provides translated strings for plugin labels
3730 *
3731 * @param string $key the key string for the label to get
3732 * @return string the translated string or the key string if not found
3733 */
3734 public static function plugin_label( $key )
3735 {
3736 $labels = self::apply_filters('plugin_labels', array(
3737 'add_participant' => __( 'Add Participant', 'participants-database' ),
3738 'list_participants' => __( 'List Participants', 'participants-database' ),
3739 'manage_fields' => __( 'Manage Database Fields', 'participants-database' ),
3740 'upload_csv' => __( 'Import CSV File', 'participants-database' ),
3741 'plugin_settings' => '<span class="dashicons dashicons-admin-generic"></span>' . __( 'Settings', 'participants-database' ),
3742 'setup_guide' => __( 'Setup Guide', 'participants-database' ),
3743 'add_record_title' => __( 'Add New Participant Record', 'participants-database' ),
3744 'edit_record_title' => __( 'Edit Existing Participant Record', 'participants-database' ),
3745 'list_participants_title' => __( 'List Participants', 'participants-database' ),
3746 'export_csv_title' => __( 'Export CSV', 'participants-database' ),
3747 'manage_list_columns' => __( 'Manage List Columns', 'participants-database' ),
3748 ) );
3749 return isset( $labels[$key] ) ? $labels[$key] : $key;
3750 }
3751
3752 /**
3753 * provides the plugin's data array
3754 *
3755 * @param string $plugin_file path tp the plugin file
3756 * @return array
3757 */
3758 public static function get_plugin_data( $plugin_file = '' )
3759 {
3760 return self::_get_all_plugin_data( $plugin_file );
3761 }
3762
3763 /**
3764 * parses the text header to extract plugin info
3765 *
3766 * @param string $plugin_file path to the main plugin file
3767 * @return array of plugin header values
3768 */
3769 private static function _get_all_plugin_data( $plugin_file = '' )
3770 {
3771 if ( $plugin_file === '' )
3772 {
3773 $plugin_file = __FILE__;
3774 }
3775
3776 $default_headers = [
3777 'Name' => 'Plugin Name',
3778 'PluginURI' => 'Plugin URI',
3779 'Version' => 'Version',
3780 'Description' => 'Description',
3781 'Author' => 'Author',
3782 'AuthorURI' => 'Author URI',
3783 'TextDomain' => 'Text Domain',
3784 'DomainPath' => 'Domain Path',
3785 'Network' => 'Network',
3786 ];
3787
3788 return get_file_data( $plugin_file, $default_headers, 'plugin');
3789 }
3790
3791
3792 /**
3793 * provides a single value from the plugin file header
3794 *
3795 * @param string $key the name of the field to get
3796 */
3797 private static function _get_plugin_data( $key )
3798 {
3799 return self::_get_all_plugin_data()[$key];
3800 }
3801
3802 /**
3803 * filters the plugin data
3804 *
3805 * @param array $plugins
3806 * @return array
3807 */
3808 public static function filter_plugin_data( $plugins )
3809 {
3810 $key = plugin_basename( __FILE__ );
3811 $plugin_data = $plugins[$key];
3812 $filtered_data = array(
3813 'Name' => self::apply_filters( 'plugin_title', $plugin_data['Name'] ),
3814 'Title' => self::apply_filters( 'plugin_title', $plugin_data['Title'] ),
3815 'Description' => self::apply_filters( 'plugin_description', $plugin_data['Description'] ),
3816 'Author' => self::apply_filters( 'plugin_author', $plugin_data['Author'] ),
3817 'AuthorName' => self::apply_filters( 'plugin_author', $plugin_data['AuthorName'] ),
3818 'AuthorURI' => self::apply_filters( 'plugin_author_uri', $plugin_data['AuthorURI'] ),
3819 );
3820 $plugins[$key] = array_merge( (array) $plugin_data, $filtered_data );
3821 return $plugins;
3822 }
3823
3824 /**
3825 * filters the plugins action links shown on the plugins page to add a link to
3826 * the settings page
3827 *
3828 * @param array $links
3829 * @return array
3830 */
3831 public static function add_plugin_action_links( $links )
3832 {
3833 return array_merge( $links, array('settings' => '<a href="' . admin_url( 'admin.php?page=participants-database_settings_page' ) . '">' . __( 'Settings', 'participants-database' ) . '</a>') );
3834 }
3835
3836 /**
3837 * adds links and modifications to plugin list meta row
3838 *
3839 * @param array $links
3840 * @param string $file
3841 * @return array
3842 */
3843 public static function add_plugin_meta_links( $links, $file )
3844 {
3845
3846 // create link
3847 if ( $file == plugin_basename( __FILE__ ) ) {
3848
3849 //error_log( ' meta links: '.print_r( $links,1 ));
3850
3851 $links[1] = str_replace( self::_get_plugin_data( 'Author' ), 'xn*au webdesign', $links[1] );
3852 // $links[] = '<a href="http://wordpress.org/support/view/plugin-reviews/participants-database">' . __( 'Submit a rating or review', 'participants-database' ) . ' </a>';
3853 // $links[] = '<span style="color:#6B4001;">' . __( 'Free tech support and continued development relies on your support:', 'participants-database' ) . ' <a class="button xnau-contribute" href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=6C7FSX2DQFWY4">' . __( 'contribute', 'participants-database' ) . '</a></span>';
3854 }
3855 return $links;
3856 }
3857
3858 }
3859
3860 // class
3861
3862 /**
3863 * performs the class autoload
3864 *
3865 * @param string $class the name of the class to be loaded
3866 */
3867 function PDb_class_loader( $class )
3868 {
3869 if ( !class_exists( $class ) ) {
3870
3871 $file = ltrim(str_replace('\\', '/', $class), '/') . '.php';
3872
3873 /**
3874 * allows class overrides
3875 *
3876 * @filter pdb-autoloader_class_path
3877 * @param string the absolute path to the class file
3878 * @return string
3879 */
3880 $class_file = apply_filters( 'pdb-autoloader_class_path', plugin_dir_path( __FILE__ ) . 'classes/' . $file ); // $class . '.class.php'
3881
3882 if ( is_file( $class_file ) ) {
3883
3884 require_once $class_file;
3885 }
3886 }
3887 }
3888
3889 /**
3890 * PHP version checks and notices before initializing the plugin
3891 */
3892 if ( version_compare( PHP_VERSION, Participants_Db::min_php_version, '>=' ) ) {
3893
3894 if ( is_null( Participants_Db::$plugin_version ) ) { // check for the uninitialized class
3895 Participants_Db::initialize();
3896 }
3897
3898 } else {
3899
3900 add_action( 'admin_notices', 'pdb_handle_php_version_error' );
3901
3902 add_action( 'admin_init', 'pdb_deactivate_plugin' );
3903 }
3904
3905 function pdb_deactivate_plugin()
3906 {
3907 deactivate_plugins( plugin_basename( __FILE__ ) );
3908 }
3909
3910 function pdb_handle_php_version_error()
3911 {
3912 echo '<div class="notice notice-error is-dismissible"><p><span class="dashicons dashicons-warning"></span>' . sprintf( __( 'Participants Database requires PHP version %s to function properly, you have PHP version %s. Please upgrade PHP. The Plugin has been auto-deactivated.', 'participants-database' ), Participants_Db::min_php_version, PHP_VERSION ) . '</p></div>';
3913 if ( isset( $_GET['activate'] ) ) {
3914 unset( $_GET['activate'] );
3915 }
3916 }
3917