diff --git a/.distignore b/.distignore index 7fffd9613d..a7f3e9bdb3 100644 --- a/.distignore +++ b/.distignore @@ -27,6 +27,8 @@ .gitignore .jshintrc .nvmrc +.phpcs.compat.yml +.phpcs.yml .phpstorm.meta.php .scrutinizer.yml .travis.yml @@ -39,13 +41,13 @@ codeception.example.yml codeception.slic.yml CODEOWNERS composer.json -CONTRIBUTING.md +composer.lock Gruntfile.js -jest-setup-wordpress-globals.js jest.config.json jest.config.js -package-lock.json +jest-setup-wordpress-globals.js package.json +package-lock.json phpcs.xml phpcs.xml.dist phpunit.xml.dist diff --git a/.env.testing.slic b/.env.testing.slic index b07b704218..ae465bc7ea 100644 --- a/.env.testing.slic +++ b/.env.testing.slic @@ -1,6 +1,8 @@ # Custom vars for Pods. PODS_LOAD_DATA=true PODS_REBUILD_DATA=false +# PODS_ROOT_FOLDER="pods" +# Set this back to "pods" when done testing PODS_ROOT_FOLDER="pods" # This file will be consumed by both the CI and the tests. diff --git a/.github/workflows/tests-php.yml b/.github/workflows/tests-php.yml index bd83429501..aecaa7e2ae 100644 --- a/.github/workflows/tests-php.yml +++ b/.github/workflows/tests-php.yml @@ -97,7 +97,7 @@ jobs: - name: Set up pods run: | docker network prune -f - ${SLIC_BIN} use pods + ${SLIC_BIN} use ${GITHUB_REPOSITORY#*/} ${SLIC_BIN} composer set-version 2 ${SLIC_BIN} composer install # ------------------------------------------------------------------------------ diff --git a/classes/Pods.php b/classes/Pods.php index 3e091df7de..8afaf099a0 100644 --- a/classes/Pods.php +++ b/classes/Pods.php @@ -1372,10 +1372,8 @@ public function field( $name, $single = null, $raw = false ) { continue; } - // Bypass pass field. - if ( isset( $item->user_pass ) ) { - unset( $item->user_pass ); - } + // Bypass sensitive fields. + $item = pods_access_bleep_data( $item ); // Get Item ID. $item_id = $item->pod_item_id; @@ -1388,6 +1386,10 @@ public function field( $name, $single = null, $raw = false ) { } elseif ( 'objects' === $params->output ) { if ( in_array( $object_type, array( 'post_type', 'media' ), true ) ) { $item = get_post( $item_id ); + + if ( ! empty( $item ) ) { + $item = pods_access_bleep_data( $item ); + } } elseif ( 'taxonomy' === $object_type ) { $item = get_term( $item_id, $object ); } elseif ( 'user' === $object_type ) { @@ -1406,7 +1408,7 @@ public function field( $name, $single = null, $raw = false ) { $item->caps = $caps; $item->allcaps = $allcaps; - unset( $item->user_pass ); + $item = pods_access_bleep_data( $item ); } } elseif ( 'comment' === $object_type ) { $item = get_comment( $item_id ); @@ -3556,20 +3558,6 @@ public function helper( $helper, $value = null, $name = null ) { $params = array_merge( $params, $helper ); } - /** - * Allows changing whether callbacks are allowed to run. - * - * @param bool $allow_callbacks Whether callbacks are allowed to run. - * @param array $params Parameters used by Pods::helper() method. - * - * @since 2.8.0 - */ - $allow_callbacks = (boolean) apply_filters( 'pods_helper_allow_callbacks', true, $params ); - - if ( ! $allow_callbacks ) { - return $value; - } - /** * Allows changing whether to include the Pods object as the second value to the callback. * @@ -3580,111 +3568,25 @@ public function helper( $helper, $value = null, $name = null ) { */ $include_obj = (boolean) apply_filters( 'pods_helper_include_obj', false, $params ); - if ( ! is_callable( $params['helper'] ) ) { - if ( $include_obj ) { - return apply_filters( $params['helper'], $value, $this ); - } else { - return apply_filters( $params['helper'], $value ); - } - } - - $disallowed = [ - // Regex related. - 'preg_replace', - 'preg_replace_array', - 'preg_replace_callback', - 'preg_replace_callback_array', - 'preg_match', - 'preg_match_all', - // Eval related. - 'system', - 'exec', - 'eval', - 'create_function', - // File related. - 'popen', - 'include', - 'include_once', - 'require', - 'require_once', - 'file_get_contents', - 'file_put_contents', - 'get_template_part', - // Nonce related. - 'wp_nonce_url', - 'wp_nonce_field', - 'wp_create_nonce', - 'check_admin_referer', - 'check_ajax_referer', - 'wp_verify_nonce', - // PHP related. - 'constant', - 'defined', - 'get_current_user', - 'get_defined_constants', - 'get_defined_functions', - 'get_defined_vars', - 'get_extension_funcs', - 'get_include_path', - 'get_included_files', - 'get_loaded_extensions', - 'get_required_files', - 'get_resources', - 'getenv', - 'getopt', - 'ini_alter', - 'ini_get', - 'ini_get_all', - 'ini_restore', - 'ini_set', - 'php_ini_loaded_file', - 'php_ini_scanned_files', - 'php_sapi_name', - 'php_uname', - 'phpinfo', - 'phpversion', - 'putenv', - ]; - - $allowed = []; - - /** - * Allows adjusting the disallowed callbacks as needed. - * - * @param array $disallowed List of callbacks not allowed. - * @param array $params Parameters used by Pods::helper() method. - * - * @since 2.7.0 - */ - $disallowed = apply_filters( 'pods_helper_disallowed_callbacks', $disallowed, $params ); - - /** - * Allows adjusting the allowed callbacks as needed. - * - * @param array $allowed List of callbacks explicitly allowed. - * @param array $params Parameters used by Pods::helper() method. - * - * @since 2.7.0 - */ - $allowed = apply_filters( 'pods_helper_allowed_callbacks', $allowed, $params ); - // Clean up helper callback (if string). if ( is_string( $params['helper'] ) ) { $params['helper'] = strip_tags( str_replace( array( '`', chr( 96 ) ), "'", $params['helper'] ) ); } - $is_allowed = false; + if ( ! pods_access_callback_allowed( $params['helper'], $params ) ) { + return $value; + } - if ( ! empty( $allowed ) ) { - if ( in_array( $params['helper'], $allowed, true ) ) { - $is_allowed = true; + if ( ! is_callable( $params['helper'] ) ) { + if ( ! is_string( $params['helper'] ) ) { + return ''; } - } elseif ( ! in_array( $params['helper'], $disallowed, true ) ) { - $is_allowed = true; - } - if ( ! $is_allowed ) { - return $value; + if ( $include_obj ) { + return apply_filters( $params['helper'], $value, $this ); + } + + return apply_filters( $params['helper'], $value ); } try { @@ -3746,20 +3648,21 @@ public function template_singular( $template_name, $code = null, $deprecated = f * @param string $template_name The template name. * @param string|null $code Custom template code to use instead. * @param bool $deprecated Whether to use deprecated functionality based on old function usage. + * @param bool $check_access Whether to check access for Posts that are Password-protected. * * @return mixed Template output * * @since 2.0.0 * @link https://docs.pods.io/code/pods/template/ */ - public function template( $template_name, $code = null, $deprecated = false ) { + public function template( $template_name, $code = null, $deprecated = false, $check_access = false ) { $out = null; $obj =& $this; if ( class_exists( 'Pods_Templates' ) ) { - $out = Pods_Templates::template( $template_name, $code, $this, $deprecated ); + $out = Pods_Templates::template( $template_name, $code, $this, $deprecated, $check_access ); } elseif ( ! empty( $code ) ) { // backwards compatibility. $code = str_replace( '$this->', '$obj->', $code ); @@ -3782,18 +3685,43 @@ public function template( $template_name, $code = null, $deprecated = false ) { */ $code = apply_filters( "pods_templates_pre_template_{$template_name}", $code, $template_name, $this ); + $info = $check_access ? pods_info_from_args( [ 'pods' => $this ] ) : []; + ob_start(); if ( ! empty( $code ) ) { // Only detail templates need $this->id. if ( empty( $this->id ) ) { while ( $this->fetch() ) { + $info['item_id'] = $this->id(); + + // Ensure the post is not password protected. + if ( + $check_access + && ( + pods_access_bypass_post_with_password( $info ) + || pods_access_bypass_private_post( $info ) + ) + ) { + continue; + } + // @codingStandardsIgnoreLine echo $this->do_magic_tags( $code ); } } else { - // @codingStandardsIgnoreLine - echo $this->do_magic_tags( $code ); + $info['item_id'] = $this->id(); + + if ( + ! $check_access + || ( + ! pods_access_bypass_post_with_password( $info ) + && ! pods_access_bypass_private_post( $info ) + ) + ) { + // @codingStandardsIgnoreLine + echo $this->do_magic_tags( $code ); + } } } @@ -3846,10 +3774,33 @@ public function template( $template_name, $code = null, $deprecated = false ) { // Only detail templates need $this->id. if ( empty( $this->id ) ) { while ( $this->fetch() ) { + $info['item_id'] = $this->id(); + + // Ensure the post is not password protected. + if ( + $check_access + && ( + pods_access_bypass_post_with_password( $info ) + || pods_access_bypass_private_post( $info ) + ) + ) { + continue; + } + pods_template_part( $default_templates, compact( array_keys( get_defined_vars() ) ) ); } } else { - pods_template_part( $default_templates, compact( array_keys( get_defined_vars() ) ) ); + $info['item_id'] = $this->id(); + + if ( + ! $check_access + || ( + ! pods_access_bypass_post_with_password( $info ) + && ! pods_access_bypass_private_post( $info ) + ) + ) { + pods_template_part( $default_templates, compact( array_keys( get_defined_vars() ) ) ); + } } $out = ob_get_clean(); @@ -3918,7 +3869,7 @@ public function form( $params = null, $label = null, $thank_you = null ) { esc_html__( 'Anonymous form submissions are not enabled for this site', 'pods' ), esc_url( wp_login_url( pods_current_url() ) ), esc_html__( 'try logging in first', 'pods' ), - esc_html__( 'contacting your site administrator', 'pods' ) + esc_html__( 'or contacting your site administrator', 'pods' ) ); } @@ -3943,13 +3894,15 @@ public function form( $params = null, $label = null, $thank_you = null ) { } } - $defaults = array( - 'fields' => $params, - 'label' => $label, - 'thank_you' => $thank_you, - 'fields_only' => false, - 'output_type' => 'div', - ); + $defaults = [ + 'fields' => $params, + 'label' => $label, + 'thank_you' => $thank_you, + 'fields_only' => false, + 'output_type' => 'div', + 'check_access' => true, + 'form_key' => null, + ]; if ( is_array( $params ) ) { $params = array_merge( $defaults, $params ); @@ -3957,6 +3910,51 @@ public function form( $params = null, $label = null, $thank_you = null ) { $params = $defaults; } + $access_type = $this->exists() ? 'edit' : 'add'; + + $return = ''; + + // Check if the current user has access to the object. + if ( ! empty( $params['check_access'] ) ) { + $dynamic_feature_unrestricted = pods_can_use_dynamic_feature_unrestricted( + [ + 'pods' => $this, + ], + 'form', + $access_type + ); + + if ( + ! pods_current_user_can_access_object( + [ + 'pods' => $this, + ], + $access_type, + $params['form_key'] + ) + && ! $dynamic_feature_unrestricted + ) { + // Stop display and only return the notice. + return pods_get_access_user_notice( + [ + 'pods' => $this, + ], + false, + esc_html__( 'You do not have access to this embedded form.', 'pods' ) + ) ?: ''; + } + + // Show the admin-specific notice that this content may not be visible to others since it is not public. + if ( ! $dynamic_feature_unrestricted && pods_is_admin() ) { + // Include the notice in the display output to let the admin know and continue the display. + $return .= pods_get_access_admin_notice( + [ + 'pods' => $this, + ] + ) ?: ''; + } + } + $pod =& $this; $params = $this->do_hook( 'form_params', $params ); @@ -4093,7 +4091,7 @@ public function form( $params = null, $label = null, $thank_you = null ) { pods_view( PODS_DIR . 'ui/front/form.php', compact( array_keys( get_defined_vars() ) ) ); - $output = ob_get_clean(); + $output = $return . ob_get_clean(); if ( empty( $this->id ) ) { $this->row_override = array(); diff --git a/classes/PodsAPI.php b/classes/PodsAPI.php index 068723fdc0..187ea9a1ba 100644 --- a/classes/PodsAPI.php +++ b/classes/PodsAPI.php @@ -955,6 +955,7 @@ public function get_wp_object_fields( $object = 'post_type', $pod = null, $refre 'label' => 'Password', 'type' => 'password', 'alias' => [], + 'hide_in_default_form' => true, ], 'post_name' => [ 'name' => 'post_name', @@ -10512,7 +10513,7 @@ public function get_table_info( $object_type, $object, $name = null, $pod = null // Check for bad serialized array. if ( is_string( $post_status ) ) { - $post_status = maybe_unserialize( $post_status ); + $post_status = pods_maybe_safely_unserialize( $post_status ); if ( is_string( $post_status ) ) { $post_status = explode( ',', $post_status ); @@ -11394,11 +11395,20 @@ public function process_form( $form_params, $obj = null, $fields = null, $thank_ $id = pods_v_sanitized( '_pods_id', $form_params ); $uri = pods_v_sanitized( '_pods_uri', $form_params ); $form = pods_v_sanitized( '_pods_form', $form_params ); + $form_key = pods_v_sanitized( '_pods_form_key', $form_params ); $location = pods_v_sanitized( '_pods_location', $form_params ); + $obj = null; + if ( is_object( $obj ) ) { $pod = $obj->pod; $id = $obj->id(); + } elseif ( $pod ) { + $obj = pods_get_instance( $pod, $id, true ); + + if ( ! $obj || is_wp_error( $obj ) || ! $obj->is_valid() ) { + $obj = null; + } } if ( ! empty( $fields ) ) { @@ -11443,6 +11453,48 @@ public function process_form( $form_params, $obj = null, $fields = null, $thank_ $data[ $field ] = pods_v( 'pods_field_' . $field, $form_params, '' ); } + // Check if the user should have access to shortcodes/blocks. + if ( + $obj + && 'post_type' === $obj->pod_data->get_type() + && ! empty( $data['post_content'] ) + ) { + $restrict_content = ! current_user_can( 'edit_posts', $id ?: null ); + + /** + * Allow filtering whether the post content needs to be restricted to have shortcodes/blocks removed. + * + * @since 3.1.0 + * + * @param bool $restrict_content Whether the post content needs to be restricted to have shortcodes/blocks removed. + * @param string $object_type The object type. + * @param string|null $object_name The object name (if different from the object type like post_type, taxonomy, and setting have). + * @param string|int $id The item ID (if editing). + * @param string|null $form_key The form key for context. + */ + $restrict_content = (bool) apply_filters( + 'pods_api_process_form_restrict_content', + $restrict_content, + $obj->pod_data->get_type(), + $obj->pod_data->get_name(), + $id, + $form_key + ); + + if ( $restrict_content ) { + // Strip shortcodes. + $data['post_content'] = strip_shortcodes( $data['post_content'] ); + + // Allow minimal blocks. + $data['post_content'] = excerpt_remove_blocks( $data['post_content'] ); + + // Clean up the content where missing blocks might be. + $data['post_content'] = str_replace( "\n\n\n", "\n", $data['post_content'] ); + } + + $data['post_content'] = trim( $data['post_content'] ); + } + $params = array( 'pod' => $pod, 'id' => $id, diff --git a/classes/PodsAdmin.php b/classes/PodsAdmin.php index 612fd0044d..f7eb520575 100644 --- a/classes/PodsAdmin.php +++ b/classes/PodsAdmin.php @@ -552,6 +552,12 @@ public function admin_menu() { 'function' => array( $this, 'admin_components' ), 'access' => 'pods_components', ), + 'pods-access-rights-review' => array( + 'label' => __( 'Review Access Rights', 'pods' ), + 'title' => __( 'Pods Review Access Rights', 'pods' ), + 'function' => array( $this, 'admin_access_rights_review' ), + 'access' => 'pods', + ), 'pods-settings' => array( 'label' => __( 'Settings', 'pods' ), 'title' => __( 'Pods Settings', 'pods' ), @@ -1319,7 +1325,7 @@ public function admin_setup() { 'add' => [ $this, 'admin_setup_add' ], 'edit' => [ 'callback' => [ $this, 'admin_setup_edit' ], - 'restrict_callback' => [ $this, 'admin_setup_edit_restrict' ], + 'restrict_callback' => [ $this, 'admin_restrict_non_db_type' ], ], 'duplicate_pod' => [ 'label' => __( 'Duplicate', 'pods' ), @@ -1335,7 +1341,7 @@ public function admin_setup() { . "\n\n" . __( 'You may want to go to Pods Admin > Settings > Cleanup & Reset > "Delete all content for a Pod" first.', 'pods' ), 'callback' => [ $this, 'admin_setup_delete' ], - 'restrict_callback' => [ $this, 'admin_setup_delete_restrict' ], + 'restrict_callback' => [ $this, 'admin_restrict_non_db_type' ], 'nonce' => true, 'span_class' => 'delete', ], @@ -1393,692 +1399,1929 @@ public function admin_setup() { $this->handle_callouts_updates(); add_filter( 'pods_ui_manage_custom_container_classes', array( $this, 'admin_manage_container_class' ) ); - add_action( 'pods_ui_manage_after_container', array( $this, 'admin_manage_callouts' ) ); + + if ( $this->has_horizontal_callout() ) { + add_action( 'pods_ui_manage_before_container', [ $this, 'admin_manage_callouts' ] ); + } else { + add_action( 'pods_ui_manage_after_container', [ $this, 'admin_manage_callouts' ] ); + } pods_ui( $ui ); } /** - * Get list of callouts to show. - * - * @since 2.7.17 - * - * @return array List of callouts. + * Handle the Review Access Rights screen. */ - public function get_callouts() { - // Demo mode always bypasses callouts. - if ( pods_is_demo() ) { - return []; - } - - $force_callouts = false; + public function admin_access_rights_review() { + $api = pods_api(); - $page = pods_v( 'page' ); + $pods = $api->load_pods( [ 'fields' => false ] ); - if ( in_array( $page, array( 'pods-settings', 'pods-help' ), true ) ) { - $force_callouts = true; + if ( empty( $pods ) ) { + pods_message( __( 'You do not have any Pods set up yet.', 'pods ' ) ); } - $callouts = get_option( 'pods_callouts' ); + $pod_types = $api->get_pod_types(); + + $first_pods_version = get_option( 'pods_framework_version_first' ); + $first_pods_version = '' === $first_pods_version ? PODS_VERSION : $first_pods_version; + + $dynamic_features_allow_options = pods_access_get_dynamic_features_allow_options(); + $restricted_dynamic_features_options = pods_access_get_restricted_dynamic_features_options(); + + $other_view_groups = [ + 'public' => [ + 'label' => __( 'Content Privacy', 'pods' ), + 'views' => [ + '1' => [ + 'label' => __( 'Public', 'pods' ), + 'count' => 0, + ], + '0' => [ + 'label' => __( 'Private', 'pods' ), + 'count' => 0, + ], + ], + ], + 'dynamic_features_allow' => [ + 'label' => __( 'Dynamic Features', 'pods' ), + 'views' => [], + ], + 'restricted_dynamic_features' => [ + 'label' => __( 'Dynamic Features', 'pods' ), + 'views' => [ + 'unrestricted' => [ + 'label' => __( 'Unrestricted', 'pods' ), + 'count' => 0, + ], + 'restricted' => [ + 'label' => __( 'Restricted', 'pods' ), + 'count' => 0, + ], + ], + ], + ]; - if ( ! $callouts ) { - $callouts = array( - 'friends_2023_docs' => 1, - ); + foreach ( $dynamic_features_allow_options as $dynamic_features_allow_option => $dynamic_features_allow_option_label ) { + $other_view_groups['dynamic_features_allow']['views'][ $dynamic_features_allow_option ] = [ + 'label' => trim( str_replace( '🔒', '', $dynamic_features_allow_option_label ) ), + 'count' => 0, + ]; } - // Handle Friends of Pods callout logic. - $callouts['friends_2023_docs'] = ! isset( $callouts['friends_2023_docs'] ) || $callouts['friends_2023_docs'] || $force_callouts ? 1 : 0; + $other_view_groups['dynamic_features_allow']['views']['inherit']['label'] = __( 'WP Default', 'pods' ); + + $row = false; + + $pod_types_found = []; + $sources_found = []; + $source_types = []; + + $fields = [ + 'label' => [ + 'label' => __( 'Label', 'pods' ), + ], + 'name' => [ + 'label' => __( 'Name', 'pods' ), + ], + 'type' => [ + 'label' => __( 'Type', 'pods' ), + ], + 'source' => [ + 'label' => __( 'Source', 'pods' ), + 'width' => '10%', + 'type' => 'raw', + ], + 'public' => [ + 'label' => __( 'Content Privacy', 'pods' ), + ], + 'dynamic_features_allow' => [ + 'label' => __( 'Allow Dynamic Features', 'pods' ), + ], + 'restricted_dynamic_features' => [ + 'label' => __( 'Restricted Dynamic Features', 'pods' ), + 'type' => 'pick', + ], + ]; /** - * Allow hooking into whether or not the specific callouts should show. + * Filters whether to extend internal Pods. * - * @since 2.7.17 + * @since 2.8.0 * - * @param array List of callouts to enable. + * @param bool $extend_internal Whether to extend internal Pods. */ - $callouts = apply_filters( 'pods_admin_callouts', $callouts ); - - return $callouts; - } - - /** - * Handle callouts update logic. - * - * @since 2.7.17 - */ - public function handle_callouts_updates() { - $callouts = get_option( 'pods_callouts' ); + $extend_internal = apply_filters( 'pods_admin_setup_extend_pods_internal', false ); - if ( ! $callouts ) { - $callouts = array(); - } + $pod_list = array(); - $callout_dismiss = pods_v( 'pods_callout_dismiss' ); + $has_source = false; - // Demo mode will auto-update the option for future loads. - $is_demo = pods_is_demo(); + foreach ( $pods as $k => $pod ) { + $pod_type = $pod['type']; + $pod_type_label = null; - // Disable Friends of Pods callout. - if ( $is_demo || 'friends_2023_docs' === $callout_dismiss ) { - $callouts['friends_2023_docs'] = 0; + if ( empty( $pod_type ) || ! is_string( $pod_type ) ) { + $pod_type = 'post_type'; + } - update_option( 'pods_callouts', $callouts ); - } elseif ( 'reset' === $callout_dismiss ) { - $callouts = array(); + if ( ! empty( $pod['internal'] ) ) { + // Don't show internal if we aren't extending them. + if ( ! $extend_internal ) { + continue; + } - update_option( 'pods_callouts', $callouts ); - } - } + $pod_type = 'internal'; + } - /** - * Add class to container if we have callouts to show. - * - * @since 2.7.17 - * - * @param array $classes List of classes to use. - * - * @return array List of classes to use. - */ - public function admin_manage_container_class( $classes ) { - $callouts = $this->get_callouts(); + if ( isset( $pod_types[ $pod_type ] ) ) { + $pod_type_label = $pod_types[ $pod_type ]; + } - // Only get enabled callouts. - $callouts = array_filter( $callouts ); + $pod_real_type = $pod_type; - if ( ! empty( $callouts ) ) { - $classes[] = 'pods-admin--flex'; - } + if ( null !== $pod_type_label ) { + if ( ! $pod->is_extended() && in_array( $pod_type, array( + 'post_type', + 'taxonomy', + ), true ) ) { + if ( 'post_type' === $pod_type ) { + $pod_type = 'cpt'; + } else { + $pod_type = 'ct'; + } - return $classes; - } + if ( isset( $pod_types[ $pod_type ] ) ) { + $pod_type_label = $pod_types[ $pod_type ]; + } + } - /** - * Add callouts to let admins know about certain things. - * - * @since 2.7.17 - */ - public function admin_manage_callouts() { - $force_callouts = false; + if ( ! isset( $pod_types_found[ $pod_type ] ) ) { + $pod_types_found[ $pod_type ] = 1; + } else { + $pod_types_found[ $pod_type ] ++; + } - $page = pods_v( 'page' ); + $pod_real_type = $pod_type; + $pod_type = $pod_type_label; + } - if ( in_array( $page, array( 'pods-settings', 'pods-help' ), true ) ) { - $force_callouts = true; - } + $object_storage_type = $pod->get_object_storage_type(); + $source = $pod->get_object_storage_type_label(); - $callouts = $this->get_callouts(); + if ( $source ) { + $source_types[ $object_storage_type ] = $source; - if ( ! empty( $callouts['friends_2023_docs'] ) ) { - pods_view( PODS_DIR . 'ui/admin/callouts/friends_2023_docs.php', compact( array_keys( get_defined_vars() ) ) ); - } - } + if ( ! isset( $sources_found[ $object_storage_type ] ) ) { + $sources_found[ $object_storage_type ] = 1; + } else { + $sources_found[ $object_storage_type ] ++; + } + } - /** - * Get the add page of an object - * - * @param PodsUI $obj PodsUI object. - */ - public function admin_setup_add( $obj ) { - pods_view( PODS_DIR . 'ui/admin/setup-add.php', compact( array_keys( get_defined_vars() ) ) ); - } + $source = esc_html( $source ); - /** - * Get the edit page of an object - * - * @param boolean $duplicate Whether the screen is for duplicating. - * @param PodsUI $obj PodsUI object. - */ - public function admin_setup_edit( $duplicate, $obj ) { - $api = pods_api(); + if ( 'post_type' !== $object_storage_type ) { + $has_source = true; - $pod = $obj->row['pod_object']; + if ( 'file' === $object_storage_type ) { + $file_source = $pod->get_arg( '_pods_file_source' ); - if ( ! $pod instanceof Pod ) { - $obj->id = null; - $obj->row = []; - $obj->action = 'manage'; + if ( $file_source ) { + if ( 0 === strpos( $file_source, ABSPATH ) ) { + $file_source = str_replace( ABSPATH, '', $file_source ); + } - $obj->error( __( 'Invalid Pod configuration detected.', 'pods' ) ); - $obj->manage(); + ob_start(); - return null; - } + pods_help( + sprintf( + '%s: %s', + esc_html__( 'File source', 'pods' ), + esc_html( $file_source ) + ), + null, + '.pods-admin-container' + ); - if ( 'post_type' !== $pod->get_object_storage_type() ) { - $obj->id = null; - $obj->row = []; - $obj->action = 'manage'; + $source .= ' ' . ob_get_clean(); + } + } elseif ( 'collection' === $object_storage_type ) { + $code_source = $pod->get_arg( '_pods_code_source' ); - // translators: %s: The pod label. - $obj->error( sprintf( __( 'Unable to edit the "%s" Pod configuration.', 'pods' ), $pod->get_label() ) ); - $obj->manage(); + if ( $code_source ) { + if ( 0 === strpos( $code_source, ABSPATH ) ) { + $code_source = str_replace( ABSPATH, '', $code_source ); + } - return null; - } + ob_start(); - $original_field_count = 0; + pods_help( + sprintf( + '%s: %s', + esc_html__( 'Code source', 'pods' ), + esc_html( $code_source ) + ), + null, + '.pods-admin-container' + ); - foreach ( $obj->data as $row ) { - if ( (int) $row['id'] === (int) $obj->id ) { - if ( ! isset( $row['field_count'] ) ) { - $row['field_count'] = 0; + $source .= ' ' . ob_get_clean(); + } } + } - $original_field_count = (int) $row['field_count']; + $is_public = pods_is_type_public( + [ + 'pod' => $pod, + ] + ); - break; + $dynamic_features_allow_default = 'inherit'; + + if ( 'pod' === $pod->get_type() ) { + $dynamic_features_allow_default = version_compare( $first_pods_version, '3.1.0-a-1', '<' ) ? '1' : '0'; } - } - $find_orphan_fields = ( - 1 === (int) pods_v( 'pods_debug_find_orphan_fields', 'get', 0 ) - && pods_is_admin( array( 'pods' ) ) - ); + $dynamic_features_allow = $pod->get_arg( 'dynamic_features_allow', $dynamic_features_allow_default, true ); - $migrated = false; + if ( isset( $other_view_groups['dynamic_features_allow']['views'][ $dynamic_features_allow ] ) ) { + $other_view_groups['dynamic_features_allow']['views'][ $dynamic_features_allow ]['count'] ++; + } - if ( $find_orphan_fields || 1 !== (int) $pod->get_arg( '_migrated_28' ) ) { - $pod = $this->maybe_migrate_pod_fields_into_group( $pod ); + $dynamic_features_allow_label = isset( $dynamic_features_allow_options[ $dynamic_features_allow ] ) ? $dynamic_features_allow_options[ $dynamic_features_allow ] : $dynamic_features_allow_options[ $dynamic_features_allow_default ]; - $migrated = true; + if ( $dynamic_features_allow_label === $dynamic_features_allow_options['inherit'] ) { + $dynamic_features_allow_label .= ' - ' . ( $is_public ? $dynamic_features_allow_options['1'] : $dynamic_features_allow_options['0'] ); + } - // Maybe redirect the page to reload it fresh. - if ( $find_orphan_fields ) { - pods_redirect( pods_query_arg( [ 'pods_debug_find_orphan_fields' => null ] ) ); - die(); + $pod = [ + 'id' => $pod['id'], + 'label' => $pod['label'], + 'name' => $pod['name'], + 'object' => $pod['object'], + 'type' => $pod_type, + 'real_type' => $pod_real_type, + 'source' => $source, + 'real_source' => $object_storage_type, + 'bulk_disabled' => 'post_type' !== $object_storage_type, + 'pod_object' => $pod, + 'public' => $is_public ? __( 'Public', 'pods' ) : '🔒 ' . __( 'Private', 'pods' ), + 'real_public' => $is_public ? 1 : 0, + 'dynamic_features_allow' => $dynamic_features_allow_label, + 'real_dynamic_features_allow' => $dynamic_features_allow, + 'restricted_dynamic_features' => (array) $pod->get_arg( 'restricted_dynamic_features', [ + 'display', + 'form', + ] ), + ]; + + $other_view_groups['public']['views'][ (string) $pod['real_public'] ]['count'] ++; + + if ( empty( $pod['restricted_dynamic_features'] ) ) { + $pod['restricted_dynamic_features'] = __( 'Unrestricted', 'pods' ); + $pod['real_restricted_dynamic_features'] = 'unrestricted'; + } else { + foreach ( $pod['restricted_dynamic_features'] as $fk => $feature ) { + $pod['restricted_dynamic_features'][ $fk ] = pods_v( $feature, $restricted_dynamic_features_options, ucwords( $feature ) ); + } + + $pod['real_restricted_dynamic_features'] = 'restricted'; } - // Check again in case the pod migrated wrong. - if ( ! $pod instanceof Pod ) { - $obj->id = null; - $obj->row = []; - $obj->action = 'manage'; + $other_view_groups['restricted_dynamic_features']['views'][ $pod['real_restricted_dynamic_features'] ]['count'] ++; - $obj->error( __( 'Invalid Pod configuration detected.', 'pods' ) ); - $obj->manage(); + // @codingStandardsIgnoreLine + if ( 'manage' !== pods_v( 'action' ) ) { + $found_id = (int) pods_v( 'id' ); + $found_name = pods_v( 'name' ); - return null; + if ( + ( + $found_id + && $pod['id'] === $found_id + ) + || ( + $found_name + && $pod['name'] === $found_name + ) + ) { + $row = $pod; + } } + + $pod_list[] = $pod; + }//end foreach + + if ( ! $has_source ) { + unset( $fields['source'] ); } - $current_pod = $pod->export( [ - 'include_groups' => true, - 'include_group_fields' => true, - 'include_fields' => false, - ] ); + if ( false === $row && 0 < pods_v( 'id' ) && 'delete' !== pods_v( 'action' ) ) { + pods_message( 'Pod not found', 'error' ); - $group_field_count = wp_list_pluck( $current_pod['groups'], 'fields' ); - $group_field_count = array_map( 'count', $group_field_count ); - $group_field_count = array_sum( $group_field_count ); + // @codingStandardsIgnoreLine + unset( $_GET['id'], $_GET['action'] ); + } - // Detect if there may be a migration/repair needed. - if ( $original_field_count !== $group_field_count ) { - if ( ! $migrated ) { - $pod = $this->maybe_migrate_pod_fields_into_group( $pod ); + $total_pods = count( $pod_list ); - // Check again in case the pod migrated wrong. - if ( ! $pod instanceof Pod ) { - $obj->id = null; - $obj->row = []; - $obj->action = 'manage'; + $total_pods_unfiltered = $total_pods; - $obj->error( __( 'Invalid Pod configuration detected.', 'pods' ) ); - $obj->manage(); + // Handle filtering. + $view_filters = [ + 'type' => 'real_type', + 'source' => 'real_source', + 'public' => 'real_public', + 'dynamic_features_allow' => 'real_dynamic_features_allow', + 'restricted_dynamic_features' => 'real_restricted_dynamic_features', + ]; - return null; - } + $view = pods_v( 'view', 'get', 'all', true ); + $view_filter_info = explode( '/', $view ); + $view_filter_group_active = isset( $view_filter_info[0] ) ? $view_filter_info[0] : null; + $view_filter_key_active = isset( $view_filter_info[1] ) ? $view_filter_info[1] : null; - $current_pod = $pod->export( [ - 'include_groups' => true, - 'include_group_fields' => true, - 'include_fields' => false, - ] ); - } else { - pods_message( __( 'You may need to repair this Pod, we detected some fields not assigned to the groups shown in this configuration. You can find the tool at Pods Admin > Settings > Tools.', 'pods' ) ); + foreach ( $view_filters as $view_filter => $real_key ) { + if ( $view_filter_group_active !== $view_filter ) { + continue; + } + + foreach ( $pod_list as $pod_key => $pod ) { + // Maybe remove the pod from the list. + if ( (string) $view_filter_key_active !== (string) $pod[ $real_key ] ) { + unset( $pod_list[ $pod_key ] ); + } } } - $config = [ - 'currentPod' => $current_pod, - 'global' => $this->get_global_config( $pod ), - 'fieldTypes' => PodsForm::field_types(), - 'relatedObjects' => $this->get_field_related_objects(), - 'podTypes' => $api->get_pod_types(), - 'storageTypes' => $api->get_storage_types(), - // @todo SKC: Remove these below and replace any references to podsDFVConfig - 'wp_locale' => $GLOBALS['wp_locale'], - 'userLocale' => str_replace( '_', '-', get_user_locale() ), - 'currencies' => PodsField_Currency::data_currencies(), - 'datetime' => [ - 'start_of_week' => (int) get_option( 'start_of_week', 0 ), - 'gmt_offset' => (int) get_option( 'gmt_offset', 0 ), + $pod_list = wp_list_sort( array_values( $pod_list ), 'label' ); + + $total_pods = count( $pod_list ); + + $ui = [ + 'data' => $pod_list, + 'row' => $row, + 'total' => $total_pods, + 'total_found' => $total_pods, + 'items' => 'Pods', + 'item' => 'Pod', + 'header' => [ + 'manage' => '🔒 ' . __( 'Review Access Rights for Pods', 'pods' ), + ], + 'fields' => [ + 'manage' => $fields, + ], + 'sql' => [ + 'field_id' => 'id', + 'field_index' => 'label', + ], + 'actions_disabled' => [ 'add', 'view', 'export', 'delete', 'duplicate' ], + 'actions_custom' => [ + 'edit' => [ + 'label' => __( 'Edit Pod', 'pods' ), + 'restrict_callback' => [ $this, 'admin_restrict_non_db_type' ], + ], + 'make_public' => [ + 'label' => __( 'Make public', 'pods' ), + 'callback' => [ $this, 'admin_access_rights_review_make_public' ], + 'restrict_callback' => [ $this, 'admin_restrict_access_rights_review_make_public' ], + 'nonce' => true, + ], + 'make_private' => [ + 'label' => __( 'Make private', 'pods' ), + 'callback' => [ $this, 'admin_access_rights_review_make_private' ], + 'restrict_callback' => [ $this, 'admin_restrict_access_rights_review_make_private' ], + 'nonce' => true, + ], + 'wp_default_dynamic_features' => [ + 'label' => __( 'Set Dynamic Features to WP Default', 'pods' ), + 'callback' => [ $this, 'admin_access_rights_review_wp_default_dynamic_features' ], + 'restrict_callback' => [ $this, 'admin_restrict_access_rights_review_wp_default_dynamic_features' ], + 'nonce' => true, + ], + 'enable_dynamic_features' => [ + 'label' => __( 'Enable Dynamic Features', 'pods' ), + 'callback' => [ $this, 'admin_access_rights_review_enable_dynamic_features' ], + 'restrict_callback' => [ $this, 'admin_restrict_access_rights_review_enable_dynamic_features' ], + 'nonce' => true, + ], + 'disable_dynamic_features' => [ + 'label' => __( 'Disable Dynamic Features', 'pods' ), + 'callback' => [ $this, 'admin_access_rights_review_disable_dynamic_features' ], + 'restrict_callback' => [ $this, 'admin_restrict_access_rights_review_disable_dynamic_features' ], + 'nonce' => true, + ], + 'restrict_dynamic_features' => [ + 'label' => __( 'Restrict all dynamic features', 'pods' ), + 'callback' => [ $this, 'admin_access_rights_review_restrict_dynamic_features' ], + 'restrict_callback' => [ $this, 'admin_restrict_access_rights_review_restrict_dynamic_features' ], + 'nonce' => true, + ], + 'unrestrict_dynamic_features' => [ + 'label' => __( 'Unrestrict all dynamic features', 'pods' ), + 'callback' => [ $this, 'admin_access_rights_review_unrestrict_dynamic_features' ], + 'restrict_callback' => [ $this, 'admin_restrict_access_rights_review_unrestrict_dynamic_features' ], + 'nonce' => true, + ], + ], + 'actions_bulk' => [ + 'make_public' => [ + 'label' => __( 'Make public', 'pods' ), + 'callback' => [ $this, 'admin_access_rights_review_make_public_bulk' ], + ], + 'make_private' => [ + 'label' => __( 'Make private', 'pods' ), + 'callback' => [ $this, 'admin_access_rights_review_make_private_bulk' ], + ], + 'wp_default_dynamic_features' => [ + 'label' => __( 'Set Dynamic Features to WP Default', 'pods' ), + 'callback' => [ $this, 'admin_access_rights_review_wp_default_dynamic_features_bulk' ], + ], + 'enable_dynamic_features' => [ + 'label' => __( 'Enable Dynamic Features', 'pods' ), + 'callback' => [ $this, 'admin_access_rights_review_enable_dynamic_features_bulk' ], + ], + 'disable_dynamic_features' => [ + 'label' => __( 'Disable Dynamic Features', 'pods' ), + 'callback' => [ $this, 'admin_access_rights_review_disable_dynamic_features_bulk' ], + ], + 'restrict_dynamic_features' => [ + 'label' => __( 'Restrict all dynamic features', 'pods' ), + 'callback' => [ $this, 'admin_access_rights_review_restrict_dynamic_features_bulk' ], + ], + 'unrestrict_dynamic_features' => [ + 'label' => __( 'Unrestrict all dynamic features', 'pods' ), + 'callback' => [ $this, 'admin_access_rights_review_unrestrict_dynamic_features_bulk' ], + ], + ], + 'action_links' => [ + 'edit' => pods_query_arg( [ + 'page' => 'pods', + 'action' => 'edit', + 'id' => '{@id}', + 'name' => '{@name}', + ] ), ], + 'search' => false, + 'searchable' => false, + 'sortable' => true, + 'pagination' => false, ]; - $config['currentPod']['podType'] = [ - 'name' => $config['currentPod']['type'], - ]; + $ui['views'] = [ 'all' => __( 'All', 'pods' ) . ' (' . $total_pods_unfiltered . ')' ]; + $ui['view'] = $view; + $ui['heading'] = [ 'views' => __( 'View', 'pods' ) ]; + $ui['filters_enhanced'] = true; - $config['currentPod']['storageType'] = [ - 'name' => $config['currentPod']['storage'], - ]; + if ( 1 < count( $pod_types_found ) ) { + foreach ( $pod_types_found as $pod_type => $number_found ) { + $ui['views'][ 'type/' . $pod_type ] = sprintf( + '%1$s: %2$s (%3$s)', + esc_html__( 'Type', 'pods' ), + esc_html( $pod_types[ $pod_type ] ), + number_format_i18n( $number_found ) + ); + } + } - if ( ! empty( $config['currentPod']['internal'] ) ) { - $config['currentPod']['podType']['name'] = 'internal'; - } elseif ( ! $pod->is_extended() ) { - if ( 'post_type' === $config['currentPod']['type'] ) { - $config['currentPod']['podType']['name'] = 'cpt'; - } elseif ( 'taxonomy' === $config['currentPod']['type'] ) { - $config['currentPod']['podType']['name'] = 'ct'; + if ( $has_source && 1 < count( $sources_found ) ) { + foreach ( $sources_found as $source_type => $number_found ) { + $ui['views'][ 'source/' . $source_type ] = sprintf( + '%1$s: %2$s (%3$s)', + esc_html__( 'Source', 'pods' ), + esc_html( $source_types[ $source_type ] ), + number_format_i18n( $number_found ) + ); } } - $config['currentPod']['podType']['label'] = ucwords( str_replace( '_', ' ', $config['currentPod']['podType']['name'] ) ); + foreach ( $other_view_groups as $view_group_key => $view_group_info ) { + if ( empty( $view_group_info['views'] ) ) { + continue; + } - if ( ! empty( $config['podTypes'][ $config['currentPod']['podType']['name'] ] ) ) { - $config['currentPod']['podType']['label'] = $config['podTypes'][ $config['currentPod']['podType']['name'] ]; + foreach ( $view_group_info['views'] as $view_key => $view_info ) { + $ui['views'][ $view_group_key . '/' . $view_key ] = sprintf( + '%1$s: %2$s (%3$s)', + $view_group_info['label'], + esc_html( $view_info['label'] ), + number_format_i18n( $view_info['count'] ) + ); + } } - if ( 'settings' === $config['currentPod']['type'] ) { - $config['currentPod']['storageType']['name'] = 'option'; + $this->handle_callouts_updates(); + + add_action( 'pods_ui_manage_before_filters', static function() { + $callout_dismiss_link = add_query_arg( [ + 'pods_callout_dismiss' => 'access_rights', + 'pods_callout_dismiss_nonce' => wp_create_nonce( 'pods_callout_dismiss_access_rights' ), + ] ); + + pods_message( + wpautop( + esc_html__( 'This screen is for reviewing the access rights and settings for your Pods. You can change the access rights for each Pod individually or in bulk.', 'pods' ) + . "\n\n" . esc_html__( 'Carefully review whether your content types should be public and if dynamic features should be allowed.', 'pods' ) + . "\n\n" . '' . esc_html__( 'Read the documentation for more information', 'pods' ) . ' »' + ), + 'info', + false, + false + ); + + $callouts = PodsAdmin::$instance->get_callouts(); + + if ( ! empty( $callouts['access_rights'] ) ) { + pods_message( + wpautop( + esc_html__( 'You have not confirmed your Pods access rights below yet.', 'pods' ) + . "\n\n" . esc_html__( 'Make changes below or confirm them.', 'pods' ) + . "\n\n" . '' . esc_html__( 'Yes, I confirm the access rights below are correct for my site', 'pods' ) . '' + ), + 'warning', + false, + false + ); + } + + if ( ! pods_can_use_dynamic_features() ) { + pods_message( + wpautop( + sprintf( + ' + 🔒 %s + + %s » + ', + esc_html__( 'Dynamic Features are currently disabled globally which will automatically override any Dynamic Feature setting below from being referenced.', 'pods' ), + esc_url( admin_url( 'admin.php?page=pods-settings' ) ), + esc_html__( 'You can adjust this in your Pods Settings', 'pods' ) + ) + ), + 'error', + false, + false + ); + } + } ); + + pods_ui( $ui ); + } + + /** + * Get list of callouts to show. + * + * @since 2.7.17 + * + * @return array List of callouts. + */ + public function get_callouts() { + // Demo mode always bypasses callouts. + if ( pods_is_demo() ) { + return []; } - $config['currentPod']['storageType']['label'] = ucwords( $config['currentPod']['storageType']['name'] ); + $force_callouts = 1 === (int) pods_v( 'pods_force_callouts' ); - if ( ! empty( $config['storageTypes'][ $config['currentPod']['storageType']['name'] ] ) ) { - $config['currentPod']['storageType']['label'] = $config['storageTypes'][ $config['currentPod']['storageType']['name'] ]; + $page = pods_v( 'page' ); + + if ( in_array( $page, array( 'pods-settings', 'pods-help' ), true ) ) { + $force_callouts = true; + } + + $callouts = get_option( 'pods_callouts' ); + + if ( ! $callouts ) { + $callouts = [ + 'friends_2023_docs' => 1, + 'access_rights' => ( + PodsInit::$version_last + && version_compare( PodsInit::$version_last, '3.1.0-a-1', '<' ) + ) ? 1 : 0, + ]; + + update_option( 'pods_callouts', $callouts ); } + // Handle callouts logic. + $callouts['access_rights'] = ! isset( $callouts['access_rights'] ) || $callouts['access_rights'] ? 1 : 0; + $callouts['friends_2023_docs'] = ! isset( $callouts['friends_2023_docs'] ) || $callouts['friends_2023_docs'] || $force_callouts ? 1 : 0; + /** - * Allow filtering the admin config data. + * Allow hooking into whether or not the specific callouts should show. * - * @since 2.8.0 + * @since 2.7.17 * - * @param array $config The admin config data. - * @param Pod $pod The pod object. - * @param PodsUI $obj The PodsUI object. + * @param array List of callouts to enable. */ - $config = apply_filters( 'pods_admin_setup_edit_pod_config', $config, $pod, $obj ); - - wp_localize_script( 'pods-dfv', 'podsAdminConfig', $config ); + $callouts = apply_filters( 'pods_admin_callouts', $callouts ); - pods_view( PODS_DIR . 'ui/admin/setup-edit.php', compact( array_keys( get_defined_vars() ) ) ); + return $callouts; } /** - * Restrict Edit action. + * Determine whether there's a horizontal callout. * - * @param bool $restricted Whether action is restricted. - * @param array $restrict Restriction array. - * @param string $action Current action. - * @param array $row Item data row. - * @param PodsUI $obj PodsUI object. + * @since 3.1.0 * - * @since 2.3.10 + * @param array $callouts The list of callouts if available, otherwise it will be fetched. * - * @return bool + * @return bool Whether there's a horizontal callout. */ - public function admin_setup_edit_restrict( $restricted, $restrict, $action, $row, $obj ) { - if ( __( 'DB', 'pods' ) !== $row['source'] ) { - $restricted = true; + public function has_horizontal_callout( array $callouts = [] ): bool { + if ( ! $callouts ) { + $callouts = $this->get_callouts(); } - return $restricted; + return ! empty( $callouts['access_rights'] ); } /** - * Get list of field related objects. - * - * @since 2.8.0 + * Handle callouts update logic. * - * @return array List of field related objects. + * @since 2.7.17 + */ + public function handle_callouts_updates() { + $callouts = get_option( 'pods_callouts' ); + + if ( ! $callouts ) { + $callouts = array(); + } + + $callout_dismiss = sanitize_text_field( pods_v( 'pods_callout_dismiss' ) ); + $callout_dismiss_nonce = pods_v( 'pods_callout_dismiss_nonce' ); + + // Demo mode will auto-update the option for future loads. + $is_demo = pods_is_demo(); + + // Handle reset dismiss separately. + if ( 'reset' === $callout_dismiss ) { + // Reset callouts. + update_option( 'pods_callouts', [] ); + + return; + } + + // Invalid nonce, cannot do anything with this dismiss request. + if ( + ! $is_demo + && ( + ! $callout_dismiss_nonce + || false === wp_verify_nonce( $callout_dismiss_nonce, 'pods_callout_dismiss_' . $callout_dismiss ) + ) + ) { + return; + } + + if ( $is_demo ) { + // Disable Friends of Pods callout on demos. + $callout_dismiss = 'friends_2023_docs'; + } + + if ( $callout_dismiss ) { + $this->update_callout( $callout_dismiss, false ); + } + } + + /** + * Handle updating whether a callout should be enabled. + * + * @since 3.1.0 + * + * @param string $callout The callout to update. + * @param bool $enabled Whether the callout should be enabled. + */ + public function update_callout( string $callout, bool $enabled ) { + $callouts = get_option( 'pods_callouts' ); + + if ( ! $callouts ) { + $callouts = array(); + } + + $callouts[ $callout ] = (int) $enabled; + + update_option( 'pods_callouts', $callouts ); + } + + /** + * Add class to container if we have callouts to show. + * + * @since 2.7.17 + * + * @param array $classes List of classes to use. + * + * @return array List of classes to use. + */ + public function admin_manage_container_class( $classes ) { + $callouts = $this->get_callouts(); + + // Only get enabled callouts. + $callouts = array_filter( $callouts ); + + if ( ! empty( $callouts ) ) { + if ( $this->has_horizontal_callout( $callouts ) ) { + $classes[] = 'pods-admin--flex-horizontal'; + } else { + $classes[] = 'pods-admin--flex'; + } + } + + return $classes; + } + + /** + * Add callouts to let admins know about certain things. + * + * @since 2.7.17 + */ + public function admin_manage_callouts() { + static $did_callout = false; + + if ( $did_callout ) { + return; + } + + $force_callouts = false; + + $page = pods_v( 'page' ); + + if ( in_array( $page, array( 'pods-settings', 'pods-help' ), true ) ) { + $force_callouts = true; + } + + $callouts = $this->get_callouts(); + + if ( ! empty( $callouts['access_rights'] ) ) { + $did_callout = true; + + pods_view( PODS_DIR . 'ui/admin/callouts/access_rights.php', compact( array_keys( get_defined_vars() ) ) ); + } elseif ( ! empty( $callouts['friends_2023_docs'] ) ) { + $did_callout = true; + + pods_view( PODS_DIR . 'ui/admin/callouts/friends_2023_docs.php', compact( array_keys( get_defined_vars() ) ) ); + } + } + + /** + * Get the add page of an object + * + * @param PodsUI $obj PodsUI object. + */ + public function admin_setup_add( $obj ) { + pods_view( PODS_DIR . 'ui/admin/setup-add.php', compact( array_keys( get_defined_vars() ) ) ); + } + + /** + * Get the edit page of an object + * + * @param boolean $duplicate Whether the screen is for duplicating. + * @param PodsUI $obj PodsUI object. + */ + public function admin_setup_edit( $duplicate, $obj ) { + $api = pods_api(); + + $pod = $obj->row['pod_object']; + + if ( ! $pod instanceof Pod ) { + $obj->id = null; + $obj->row = []; + $obj->action = 'manage'; + + $obj->error( __( 'Invalid Pod configuration detected.', 'pods' ) ); + $obj->manage(); + + return null; + } + + if ( 'post_type' !== $pod->get_object_storage_type() ) { + $obj->id = null; + $obj->row = []; + $obj->action = 'manage'; + + // translators: %s: The pod label. + $obj->error( sprintf( __( 'Unable to edit the "%s" Pod configuration.', 'pods' ), $pod->get_label() ) ); + $obj->manage(); + + return null; + } + + $original_field_count = 0; + + foreach ( $obj->data as $row ) { + if ( (int) $row['id'] === (int) $obj->id ) { + if ( ! isset( $row['field_count'] ) ) { + $row['field_count'] = 0; + } + + $original_field_count = (int) $row['field_count']; + + break; + } + } + + $find_orphan_fields = ( + 1 === (int) pods_v( 'pods_debug_find_orphan_fields', 'get', 0 ) + && pods_is_admin( array( 'pods' ) ) + ); + + $migrated = false; + + if ( $find_orphan_fields || 1 !== (int) $pod->get_arg( '_migrated_28' ) ) { + $pod = $this->maybe_migrate_pod_fields_into_group( $pod ); + + $migrated = true; + + // Maybe redirect the page to reload it fresh. + if ( $find_orphan_fields ) { + pods_redirect( pods_query_arg( [ 'pods_debug_find_orphan_fields' => null ] ) ); + die(); + } + + // Check again in case the pod migrated wrong. + if ( ! $pod instanceof Pod ) { + $obj->id = null; + $obj->row = []; + $obj->action = 'manage'; + + $obj->error( __( 'Invalid Pod configuration detected.', 'pods' ) ); + $obj->manage(); + + return null; + } + } + + $current_pod = $pod->export( [ + 'include_groups' => true, + 'include_group_fields' => true, + 'include_fields' => false, + ] ); + + $group_field_count = wp_list_pluck( $current_pod['groups'], 'fields' ); + $group_field_count = array_map( 'count', $group_field_count ); + $group_field_count = array_sum( $group_field_count ); + + // Detect if there may be a migration/repair needed. + if ( $original_field_count !== $group_field_count ) { + if ( ! $migrated ) { + $pod = $this->maybe_migrate_pod_fields_into_group( $pod ); + + // Check again in case the pod migrated wrong. + if ( ! $pod instanceof Pod ) { + $obj->id = null; + $obj->row = []; + $obj->action = 'manage'; + + $obj->error( __( 'Invalid Pod configuration detected.', 'pods' ) ); + $obj->manage(); + + return null; + } + + $current_pod = $pod->export( [ + 'include_groups' => true, + 'include_group_fields' => true, + 'include_fields' => false, + ] ); + } else { + pods_message( __( 'You may need to repair this Pod, we detected some fields not assigned to the groups shown in this configuration. You can find the tool at Pods Admin > Settings > Tools.', 'pods' ) ); + } + } + + $config = [ + 'currentPod' => $current_pod, + 'global' => $this->get_global_config( $pod ), + 'fieldTypes' => PodsForm::field_types(), + 'relatedObjects' => $this->get_field_related_objects(), + 'podTypes' => $api->get_pod_types(), + 'storageTypes' => $api->get_storage_types(), + // @todo SKC: Remove these below and replace any references to podsDFVConfig + 'wp_locale' => $GLOBALS['wp_locale'], + 'userLocale' => str_replace( '_', '-', get_user_locale() ), + 'currencies' => PodsField_Currency::data_currencies(), + 'datetime' => [ + 'start_of_week' => (int) get_option( 'start_of_week', 0 ), + 'gmt_offset' => (int) get_option( 'gmt_offset', 0 ), + ], + ]; + + $config['currentPod']['podType'] = [ + 'name' => $config['currentPod']['type'], + ]; + + $config['currentPod']['storageType'] = [ + 'name' => $config['currentPod']['storage'], + ]; + + if ( ! empty( $config['currentPod']['internal'] ) ) { + $config['currentPod']['podType']['name'] = 'internal'; + } elseif ( ! $pod->is_extended() ) { + if ( 'post_type' === $config['currentPod']['type'] ) { + $config['currentPod']['podType']['name'] = 'cpt'; + } elseif ( 'taxonomy' === $config['currentPod']['type'] ) { + $config['currentPod']['podType']['name'] = 'ct'; + } + } + + $config['currentPod']['podType']['label'] = ucwords( str_replace( '_', ' ', $config['currentPod']['podType']['name'] ) ); + + if ( ! empty( $config['podTypes'][ $config['currentPod']['podType']['name'] ] ) ) { + $config['currentPod']['podType']['label'] = $config['podTypes'][ $config['currentPod']['podType']['name'] ]; + } + + if ( 'settings' === $config['currentPod']['type'] ) { + $config['currentPod']['storageType']['name'] = 'option'; + } + + $config['currentPod']['storageType']['label'] = ucwords( $config['currentPod']['storageType']['name'] ); + + if ( ! empty( $config['storageTypes'][ $config['currentPod']['storageType']['name'] ] ) ) { + $config['currentPod']['storageType']['label'] = $config['storageTypes'][ $config['currentPod']['storageType']['name'] ]; + } + + /** + * Allow filtering the admin config data. + * + * @since 2.8.0 + * + * @param array $config The admin config data. + * @param Pod $pod The pod object. + * @param PodsUI $obj The PodsUI object. + */ + $config = apply_filters( 'pods_admin_setup_edit_pod_config', $config, $pod, $obj ); + + wp_localize_script( 'pods-dfv', 'podsAdminConfig', $config ); + + pods_view( PODS_DIR . 'ui/admin/setup-edit.php', compact( array_keys( get_defined_vars() ) ) ); + } + + /** + * Get list of field related objects. + * + * @since 2.8.0 + * + * @return array List of field related objects. + */ + protected function get_field_related_objects() { + $related_object_groups = PodsForm::field_method( 'pick', 'related_objects', true ); + + $related_objects = []; + + foreach ( $related_object_groups as $group => $group_objects ) { + foreach ( $group_objects as $name => $label ) { + $related_objects[ $name ] = [ + 'name' => $name, + 'label' => $label, + ]; + } + } + + return $related_objects; + } + + /** + * Maybe migrate pod fields into a group (if they have no group). + * + * @since 2.8.0 + * + * @param Pod $pod The pod object. + * + * @return Pod The pod object. + */ + public function maybe_migrate_pod_fields_into_group( $pod ) { + $tool = pods_container( Repair::class ); + + $results = $tool->repair_groups_and_fields_for_pod( $pod, 'upgrade' ); + + if ( '' !== $results['message_html'] ) { + if ( 'pods' === pods_v( 'page' ) && 'edit' === pods_v( 'action' ) && 'create' === pods_v( 'do' ) ) { + // Refresh the page if we just added the Pod. + pods_redirect(); + } else { + pods_message( $results['message_html'] ); + } + } + + return $results['upgraded_pod']; + } + + /** + * Get the global config for Pods admin. + * + * @param null|\Pods\Whatsit $current_pod + * + * @return array Global config array. + *@since 2.8.0 + * + */ + public function get_global_config( $current_pod = null ) { + $config_pod = pods_container( Config_Pod::class ); + $config_group = pods_container( Config_Group::class ); + $config_field = pods_container( Config_Field::class ); + + // Pod: Backwards compatible configs and hooks. + $pod_tabs = $config_pod->get_tabs( $current_pod); + $pod_tab_options = $config_pod->get_fields( $current_pod, $pod_tabs ); + + $this->backcompat_convert_tabs_to_groups( $pod_tabs, $pod_tab_options, 'pod/_pods_pod' ); + + // If not types-only mode, handle groups/fields configs. + if ( ! pods_is_types_only( false, $current_pod->get_name() ) ) { + // Group: Backwards compatible methods and hooks. + $group_tabs = $config_group->get_tabs( $current_pod); + $group_tab_options = $config_group->get_fields( $current_pod, $group_tabs ); + + $this->backcompat_convert_tabs_to_groups( $group_tabs, $group_tab_options, 'pod/_pods_group' ); + + // Field: Backwards compatible methods and hooks. + $field_tabs = $config_field->get_tabs( $current_pod); + $field_tab_options = $config_field->get_fields( $current_pod, $field_tabs ); + + $this->backcompat_convert_tabs_to_groups( $field_tabs, $field_tab_options, 'pod/_pods_field' ); + } + + $object_collection = Pods\Whatsit\Store::get_instance(); + + /** @var Pods\Whatsit\Storage $storage */ + $storage = $object_collection->get_storage_object( 'collection' ); + + // Get objects from storage. + $pod_object = $storage->get( [ + 'object_type' => 'pod', + 'name' => '_pods_pod', + 'bypass_cache' => true, + ] ); + + $group_object = $storage->get( [ + 'object_type' => 'pod', + 'name' => '_pods_group', + 'bypass_cache' => true, + ] ); + + $field_object = $storage->get( [ + 'object_type' => 'pod', + 'name' => '_pods_field', + 'bypass_cache' => true, + ] ); + + $global_config = [ + 'showFields' => ! pods_is_types_only( false, $current_pod->get_name() ), + 'pod' => $pod_object->export( [ + 'include_groups' => true, + 'include_group_fields' => true, + 'include_fields' => false, + 'include_field_data' => true, + 'bypass_cache' => true, + 'ref_id' => 'global/' . $pod_object->get_type() . '/' . $pod_object->get_name(), + ] ), + 'group' => $group_object->export( [ + 'include_groups' => true, + 'include_group_fields' => true, + 'include_fields' => false, + 'include_field_data' => true, + 'bypass_cache' => true, + 'ref_id' => 'global/' . $pod_object->get_type() . '/' . $pod_object->get_name(), + ] ), + 'field' => $field_object->export( [ + 'include_groups' => true, + 'include_group_fields' => true, + 'include_fields' => false, + 'include_field_data' => true, + 'bypass_cache' => true, + 'ref_id' => 'global/' . $pod_object->get_type() . '/' . $pod_object->get_name(), + ] ), + ]; + + /** + * Allow hooking into the global config setup for a Pod. + * + * @param array $global_config The global config object. + * @param null|\Pods\Whatsit $current_pod The Pod object. + */ + $global_config = apply_filters( 'pods_admin_setup_global_config', $global_config, $current_pod ); + + return $global_config; + } + + /** + * Convert the tabs and their options to groups/fields in the collection storage. + * + * @since 2.8.0 + * + * @param array $tabs List of registered tabs. + * @param array $options List of tab options. + * @param string $parent The parent object to register to. + * + * @return array Global config array. + */ + protected function backcompat_convert_tabs_to_groups( array $tabs, array $options, $parent ) { + $object_collection = Pods\Whatsit\Store::get_instance(); + + /** @var Pods\Whatsit\Storage\Collection $storage */ + $storage = $object_collection->get_storage_object( 'collection' ); + + $groups = []; + $fields = []; + + foreach ( $tabs as $group_name => $group_label ) { + if ( empty( $options[ $group_name ] ) ) { + continue; + } + + if ( is_array( $group_label ) ) { + $group_args = $group_label; + } else { + $group_args = [ + 'name' => $group_name, + 'label' => $group_label, + ]; + } + + $group_args['parent'] = $parent; + + $group = new \Pods\Whatsit\Group( $group_args ); + + $groups[] = $storage->add( $group ); + + $group_fields = $options[ $group_name ]; + + $sections = false; + + foreach ( $group_fields as $field_name => $field_options ) { + // Support sections. + if ( ! isset( $field_options['label'] ) ) { + $sections = true; + } + + break; + } + + $group_sections = $group_fields; + + // Store the same whether it's a section or not. + if ( ! $sections ) { + $group_sections = [ + $group_fields, + ]; + } + + foreach ( $group_sections as $section_label => $section_fields ) { + // Add section field (maybe). + if ( ! is_int( $section_label ) ) { + $field_args = [ + 'name' => sanitize_title( $section_label ), + 'label' => $section_label, + 'type' => 'heading', + 'parent' => $parent, + 'group' => 'group/' . $parent . '/' . $group_name, + ]; + + $field = new \Pods\Whatsit\Field( $field_args ); + + $fields[] = $storage->add( $field ); + } + + if ( ! is_array( $section_fields ) ) { + continue; + } + + // Add fields for section. + foreach ( $section_fields as $field_name => $field_options ) { + $boolean_group = []; + + // Handle auto-formatting from shorthand. + if ( ! empty( $field_options['boolean_group'] ) ) { + $boolean_group = $field_options['boolean_group']; + + foreach ( $boolean_group as $bgf_key => $boolean_group_field ) { + // Make sure each field has a field name. + if ( is_string( $bgf_key ) ) { + $boolean_group[ $bgf_key ]['name'] = $bgf_key; + } + } + + $boolean_group = array_values( $boolean_group ); + + $field_options['boolean_group'] = $boolean_group; + } + + if ( empty( $field_options['type'] ) ) { + continue; + } + + // Set a unique field name for boolean group headings. + if ( ! empty( $boolean_group ) ) { + $field_options['name'] = $field_name . '_' . md5( json_encode( $boolean_group ) ); + } + + $field = $this->backcompat_convert_tabs_to_groups_setup_field( [ + 'field_name' => $field_name, + 'field_options' => $field_options, + 'parent' => $parent, + 'group_name' => $group_name, + ] ); + + $fields[] = $storage->add( $field ); + } + } + } + + return compact( 'groups', 'fields' ); + } + + /** + * Setup field for backwards compatibility tabs to groups layer. + * + * @since 2.8.0 + * + * @param array $args { + * The field arguments. + * + * @type string $field_name The field name. + * @type array $field_options The field options. + * @type string|int $parent The parent group. + * @type string $group_name The group name. + * } + * + * @return \Pods\Whatsit\Field The field object. */ - protected function get_field_related_objects() { - $related_object_groups = PodsForm::field_method( 'pick', 'related_objects', true ); + public function backcompat_convert_tabs_to_groups_setup_field( $args ) { + $field_name = $args['field_name']; + $field_options = $args['field_options']; + $parent = $args['parent']; + $group_name = $args['group_name']; - $related_objects = []; + $field_args = $field_options; - foreach ( $related_object_groups as $group => $group_objects ) { - foreach ( $group_objects as $name => $label ) { - $related_objects[ $name ] = [ - 'name' => $name, - 'label' => $label, - ]; + if ( ! isset( $field_args['name'] ) ) { + $field_args['name'] = $field_name; + } + + $field_args['parent'] = $parent; + $field_args['group'] = 'group/' . $parent . '/' . $group_name; + + $dfv_args = (object) [ + 'id' => 0, + 'name' => $field_args['name'], + 'value' => '', + 'pod' => null, + 'type' => pods_v( 'type', $field_args ), + 'options' => array_merge( [ + 'id' => 0, + ], $field_args ), + 'build_item_data' => true, + ]; + + if ( ! empty( $dfv_args->type ) ) { + $field_args = PodsForm::field_method( $dfv_args->type, 'build_dfv_field_options', $field_args, $dfv_args ); + } + + return new \Pods\Whatsit\Field( $field_args ); + } + + /** + * Duplicate a pod + * + * @param PodsUI $obj PodsUI object. + */ + public function admin_setup_duplicate( $obj ) { + $new_id = pods_api()->duplicate_pod( array( 'name' => $obj->row['name'] ) ); + + if ( 0 < $new_id ) { + pods_redirect( + pods_query_arg( + array( + 'action' => 'edit', + 'id' => $new_id, + 'do' => 'duplicate', + 'name' => null, + ) + ) + ); + + return; + } + + $obj->error( __( 'An error occurred, the Pod was not duplicated.', 'pods' ) ); + } + + /** + * Restrict Duplicate action to custom types, not extended + * + * @param bool $restricted Whether action is restricted. + * @param array $restrict Restriction array. + * @param string $action Current action. + * @param array $row Item data row. + * @param PodsUI $obj PodsUI object. + * + * @since 2.3.10 + * + * @return bool + */ + public function admin_setup_duplicate_restrict( $restricted, $restrict, $action, $row, $obj ) { + if ( in_array( + $row['real_type'], array( + 'user', + 'media', + 'comment', + ), true + ) ) { + $restricted = true; + } + + return $restricted; + } + + /** + * Delete a pod + * + * @param PodsUI $obj PodsUI object. + * @param int|string $id Item ID. + * + * @return mixed + */ + public function admin_setup_delete( $obj, $id ) { + + $pod = pods_api()->load_pod( array( 'id' => $id ), false ); + + if ( empty( $pod ) ) { + return $obj->error( __( 'Pod not found.', 'pods' ) ); + } + + if ( 'post_type' !== $pod->get_object_storage_type() ) { + return $obj->error( __( 'Pod cannot be deleted.', 'pods' ) ); + } + + pods_api()->delete_pod( array( 'id' => $id ) ); + + foreach ( $obj->data as $key => $data_pod ) { + if ( (int) $id === (int) $data_pod['id'] ) { + unset( $obj->data[ $key ] ); + } + } + + $obj->total = count( $obj->data ); + $obj->total_found = count( $obj->data ); + + $obj->message( __( 'Pod deleted successfully.', 'pods' ) ); + + $obj->manage(); + } + + /** + * Handle access rights review bulk action to make public for a pod. + * + * @since 3.1.0 + * + * @param int[]|string[] $ids Item IDs. + * @param PodsUI $obj PodsUI object. + * + * @return false This always returns false unless there was a real error. + */ + public function admin_access_rights_review_make_public_bulk( $ids, $obj ) { + foreach ( $ids as $id ) { + if ( ! $id ) { + continue; + } + + $this->admin_access_rights_review_make_public( $obj, $id, 'bulk' ); + } + + $obj->message( __( 'Selected Pod(s) made public successfully.', 'pods' ) ); + + $this->update_callout( 'access_rights', false ); + + return false; + } + + /** + * Handle access rights review action to make public for a pod. + * + * @since 3.1.0 + * + * @param PodsUI $obj PodsUI object. + * @param int|string $id Item ID. + * @param string $mode Action mode. + * + * @return mixed + */ + public function admin_access_rights_review_make_public( $obj, $id, $mode = 'single' ) { + $pod = pods_api()->load_pod( [ 'id' => $id ], false ); + + if ( empty( $pod ) ) { + return 'bulk' !== $mode ? $obj->error( __( 'Pod not found.', 'pods' ) ) : false; + } + + if ( 'post_type' !== $pod->get_object_storage_type() || $pod->is_extended() ) { + return 'bulk' !== $mode ? $obj->error( __( 'Pod cannot be modified.', 'pods' ) ) : false; + } + + pods_api()->save_pod( [ 'id' => $id, 'public' => 1 ] ); + + foreach ( $obj->data as $key => $data_pod ) { + if ( (int) $id === (int) $data_pod['id'] ) { + $obj->data[ $key ]['public'] = __( 'Public', 'pods' ); + $obj->data[ $key ]['real_public'] = 1; + } + } + + if ( 'bulk' !== $mode ) { + $obj->message( __( 'Pod made public successfully.', 'pods' ) ); + + $this->update_callout( 'access_rights', false ); + + $obj->manage(); + } + } + + /** + * Handle access rights review bulk action to make private for a pod. + * + * @since 3.1.0 + * + * @param int[]|string[] $ids Item IDs. + * @param PodsUI $obj PodsUI object. + * + * @return false This always returns false unless there was a real error. + */ + public function admin_access_rights_review_make_private_bulk( $ids, $obj ) { + foreach ( $ids as $id ) { + if ( ! $id ) { + continue; + } + + $this->admin_access_rights_review_make_private( $obj, $id, 'bulk' ); + } + + $obj->message( __( 'Selected Pod(s) made private successfully.', 'pods' ) ); + + $this->update_callout( 'access_rights', false ); + + return false; + } + + /** + * Handle access rights review action to make private for a pod. + * + * @since 3.1.0 + * + * @param PodsUI $obj PodsUI object. + * @param int|string $id Item ID. + * @param string $mode Action mode. + * + * @return mixed + */ + public function admin_access_rights_review_make_private( $obj, $id, $mode = 'single' ) { + $pod = pods_api()->load_pod( [ 'id' => $id ], false ); + + if ( empty( $pod ) ) { + return 'bulk' !== $mode ? $obj->error( __( 'Pod not found.', 'pods' ) ) : false; + } + + if ( 'post_type' !== $pod->get_object_storage_type() || $pod->is_extended() ) { + return 'bulk' !== $mode ? $obj->error( __( 'Pod cannot be modified.', 'pods' ) ) : false; + } + + pods_api()->save_pod( [ 'id' => $id, 'public' => 0 ] ); + + foreach ( $obj->data as $key => $data_pod ) { + if ( (int) $id === (int) $data_pod['id'] ) { + $obj->data[ $key ]['public'] = '🔒 ' . __( 'Private', 'pods' ); + $obj->data[ $key ]['real_public'] = 0; } } - return $related_objects; + if ( 'bulk' !== $mode ) { + $obj->message( __( 'Pod made private successfully.', 'pods' ) ); + + $this->update_callout( 'access_rights', false ); + + $obj->manage(); + } } /** - * Maybe migrate pod fields into a group (if they have no group). + * Handle access rights review bulk action to restrict dynamic features for a pod. * - * @since 2.8.0 + * @since 3.1.0 * - * @param Pod $pod The pod object. + * @param int[]|string[] $ids Item IDs. + * @param PodsUI $obj PodsUI object. * - * @return Pod The pod object. + * @return false This always returns false unless there was a real error. */ - public function maybe_migrate_pod_fields_into_group( $pod ) { - $tool = pods_container( Repair::class ); - - $results = $tool->repair_groups_and_fields_for_pod( $pod, 'upgrade' ); - - if ( '' !== $results['message_html'] ) { - if ( 'pods' === pods_v( 'page' ) && 'edit' === pods_v( 'action' ) && 'create' === pods_v( 'do' ) ) { - // Refresh the page if we just added the Pod. - pods_redirect(); - } else { - pods_message( $results['message_html'] ); + public function admin_access_rights_review_restrict_dynamic_features_bulk( $ids, $obj ) { + foreach ( $ids as $id ) { + if ( ! $id ) { + continue; } + + $this->admin_access_rights_review_restrict_dynamic_features( $obj, $id, 'bulk' ); } - return $results['upgraded_pod']; + $obj->message( __( 'Selected Pod(s) restricted dynamic features successfully.', 'pods' ) ); + + $this->update_callout( 'access_rights', false ); + + return false; } /** - * Get the global config for Pods admin. + * Handle access rights review action to restrict dynamic features for a pod. * - * @param null|\Pods\Whatsit $current_pod + * @since 3.1.0 * - * @return array Global config array. - *@since 2.8.0 + * @param PodsUI $obj PodsUI object. + * @param int|string $id Item ID. + * @param string $mode Action mode. * + * @return mixed */ - public function get_global_config( $current_pod = null ) { - $config_pod = pods_container( Config_Pod::class ); - $config_group = pods_container( Config_Group::class ); - $config_field = pods_container( Config_Field::class ); - - // Pod: Backwards compatible configs and hooks. - $pod_tabs = $config_pod->get_tabs( $current_pod); - $pod_tab_options = $config_pod->get_fields( $current_pod, $pod_tabs ); + public function admin_access_rights_review_restrict_dynamic_features( $obj, $id, $mode = 'single' ) { + $pod = pods_api()->load_pod( [ 'id' => $id ], false ); - $this->backcompat_convert_tabs_to_groups( $pod_tabs, $pod_tab_options, 'pod/_pods_pod' ); + if ( empty( $pod ) ) { + return 'bulk' !== $mode ? $obj->error( __( 'Pod not found.', 'pods' ) ) : false; + } - // If not types-only mode, handle groups/fields configs. - if ( ! pods_is_types_only( false, $current_pod->get_name() ) ) { - // Group: Backwards compatible methods and hooks. - $group_tabs = $config_group->get_tabs( $current_pod); - $group_tab_options = $config_group->get_fields( $current_pod, $group_tabs ); + if ( 'post_type' !== $pod->get_object_storage_type() ) { + return 'bulk' !== $mode ? $obj->error( __( 'Pod cannot be modified.', 'pods' ) ) : false; + } - $this->backcompat_convert_tabs_to_groups( $group_tabs, $group_tab_options, 'pod/_pods_group' ); + pods_api()->save_pod( [ + 'id' => $id, + 'restricted_dynamic_features' => [ + 'display', + 'form', + ], + ] ); - // Field: Backwards compatible methods and hooks. - $field_tabs = $config_field->get_tabs( $current_pod); - $field_tab_options = $config_field->get_fields( $current_pod, $field_tabs ); + foreach ( $obj->data as $key => $data_pod ) { + if ( (int) $id === (int) $data_pod['id'] ) { + $obj->data[ $key ]['restricted_dynamic_features'] = array_map( + static function ( $label ) { + return '🔒 ' . $label; + }, + pods_access_get_restricted_dynamic_features_options() + ); - $this->backcompat_convert_tabs_to_groups( $field_tabs, $field_tab_options, 'pod/_pods_field' ); + $obj->data[ $key ]['real_restricted_dynamic_features'] = 'restricted'; + } } - $object_collection = Pods\Whatsit\Store::get_instance(); + if ( 'bulk' !== $mode ) { + $obj->message( __( 'Pod restricted dynamic features successfully.', 'pods' ) ); - /** @var Pods\Whatsit\Storage $storage */ - $storage = $object_collection->get_storage_object( 'collection' ); + $this->update_callout( 'access_rights', false ); - // Get objects from storage. - $pod_object = $storage->get( [ - 'object_type' => 'pod', - 'name' => '_pods_pod', - 'bypass_cache' => true, - ] ); + $obj->manage(); + } + } - $group_object = $storage->get( [ - 'object_type' => 'pod', - 'name' => '_pods_group', - 'bypass_cache' => true, - ] ); + /** + * Handle access rights review bulk action to restrict dynamic features for a pod. + * + * @since 3.1.0 + * + * @param int[]|string[] $ids Item IDs. + * @param PodsUI $obj PodsUI object. + * + * @return false This always returns false unless there was a real error. + */ + public function admin_access_rights_review_unrestrict_dynamic_features_bulk( $ids, $obj ) { + foreach ( $ids as $id ) { + if ( ! $id ) { + continue; + } - $field_object = $storage->get( [ - 'object_type' => 'pod', - 'name' => '_pods_field', - 'bypass_cache' => true, - ] ); + $this->admin_access_rights_review_unrestrict_dynamic_features( $obj, $id, 'bulk' ); + } - $global_config = [ - 'showFields' => ! pods_is_types_only( false, $current_pod->get_name() ), - 'pod' => $pod_object->export( [ - 'include_groups' => true, - 'include_group_fields' => true, - 'include_fields' => false, - 'include_field_data' => true, - 'bypass_cache' => true, - 'ref_id' => 'global/' . $pod_object->get_type() . '/' . $pod_object->get_name(), - ] ), - 'group' => $group_object->export( [ - 'include_groups' => true, - 'include_group_fields' => true, - 'include_fields' => false, - 'include_field_data' => true, - 'bypass_cache' => true, - 'ref_id' => 'global/' . $pod_object->get_type() . '/' . $pod_object->get_name(), - ] ), - 'field' => $field_object->export( [ - 'include_groups' => true, - 'include_group_fields' => true, - 'include_fields' => false, - 'include_field_data' => true, - 'bypass_cache' => true, - 'ref_id' => 'global/' . $pod_object->get_type() . '/' . $pod_object->get_name(), - ] ), - ]; + $obj->message( __( 'Selected Pod(s) unrestricted dynamic features successfully.', 'pods' ) ); - /** - * Allow hooking into the global config setup for a Pod. - * - * @param array $global_config The global config object. - * @param null|\Pods\Whatsit $current_pod The Pod object. - */ - $global_config = apply_filters( 'pods_admin_setup_global_config', $global_config, $current_pod ); + $this->update_callout( 'access_rights', false ); - return $global_config; + return false; } /** - * Convert the tabs and their options to groups/fields in the collection storage. + * Handle access rights review action to unrestrict dynamic features for a pod. * - * @since 2.8.0 + * @since 3.1.0 * - * @param array $tabs List of registered tabs. - * @param array $options List of tab options. - * @param string $parent The parent object to register to. + * @param PodsUI $obj PodsUI object. + * @param int|string $id Item ID. + * @param string $mode Action mode. * - * @return array Global config array. + * @return mixed */ - protected function backcompat_convert_tabs_to_groups( array $tabs, array $options, $parent ) { - $object_collection = Pods\Whatsit\Store::get_instance(); + public function admin_access_rights_review_unrestrict_dynamic_features( $obj, $id, $mode = 'single' ) { + $pod = pods_api()->load_pod( [ 'id' => $id ], false ); - /** @var Pods\Whatsit\Storage\Collection $storage */ - $storage = $object_collection->get_storage_object( 'collection' ); + if ( empty( $pod ) ) { + return 'bulk' !== $mode ? $obj->error( __( 'Pod not found.', 'pods' ) ) : false; + } - $groups = []; - $fields = []; + if ( 'post_type' !== $pod->get_object_storage_type() ) { + return 'bulk' !== $mode ? $obj->error( __( 'Pod cannot be modified.', 'pods' ) ) : false; + } - foreach ( $tabs as $group_name => $group_label ) { - if ( empty( $options[ $group_name ] ) ) { - continue; - } + pods_api()->save_pod( [ + 'id' => $id, + 'restricted_dynamic_features' => [], + ] ); - if ( is_array( $group_label ) ) { - $group_args = $group_label; - } else { - $group_args = [ - 'name' => $group_name, - 'label' => $group_label, - ]; + foreach ( $obj->data as $key => $data_pod ) { + if ( (int) $id === (int) $data_pod['id'] ) { + $obj->data[ $key ]['restricted_dynamic_features'] = __( 'Unrestricted', 'pods' ); + $obj->data[ $key ]['real_restricted_dynamic_features'] = 'unrestricted'; } + } - $group_args['parent'] = $parent; + if ( 'bulk' !== $mode ) { + $obj->message( __( 'Pod unrestricted dynamic features successfully.', 'pods' ) ); - $group = new \Pods\Whatsit\Group( $group_args ); + $this->update_callout( 'access_rights', false ); - $groups[] = $storage->add( $group ); + $obj->manage(); + } + } - $group_fields = $options[ $group_name ]; + /** + * Restrict actions that can't be done for Pods with a non DB source. + * + * @since 3.1.0 + * + * @param bool $restricted Whether action is restricted. + * @param array $restrict Restriction array. + * @param string $action Current action. + * @param array $row Item data row. + * @param PodsUI $obj PodsUI object. + * + * @return bool Whether the action is restricted. + */ + public function admin_restrict_non_db_type( $restricted, $restrict, $action, $row, $obj ) { + if ( __( 'DB', 'pods' ) !== $row['source'] ) { + $restricted = true; + } - $sections = false; + return $restricted; + } - foreach ( $group_fields as $field_name => $field_options ) { - // Support sections. - if ( ! isset( $field_options['label'] ) ) { - $sections = true; - } + /** + * Restrict actions that can't be done for Pods with a non DB source. + * + * @since 3.1.0 + * + * @param bool $restricted Whether action is restricted. + * @param array $restrict Restriction array. + * @param string $action Current action. + * @param array $row Item data row. + * @param PodsUI $obj PodsUI object. + * + * @return bool Whether the action is restricted. + */ + public function admin_restrict_access_rights_review_make_public( $restricted, $restrict, $action, $row, $obj ) { + $restricted = $this->admin_restrict_non_db_type( $restricted, $restrict, $action, $row, $obj ); - break; - } + if ( + ! $restricted + && ( + 1 === $row['real_public'] + || $row['pod_object']->is_extended() + ) + ) { + $restricted = true; + } - $group_sections = $group_fields; + return $restricted; + } - // Store the same whether it's a section or not. - if ( ! $sections ) { - $group_sections = [ - $group_fields, - ]; - } + /** + * Restrict actions that can't be done for Pods with a non DB source. + * + * @since 3.1.0 + * + * @param bool $restricted Whether action is restricted. + * @param array $restrict Restriction array. + * @param string $action Current action. + * @param array $row Item data row. + * @param PodsUI $obj PodsUI object. + * + * @return bool Whether the action is restricted. + */ + public function admin_restrict_access_rights_review_make_private( $restricted, $restrict, $action, $row, $obj ) { + $restricted = $this->admin_restrict_non_db_type( $restricted, $restrict, $action, $row, $obj ); - foreach ( $group_sections as $section_label => $section_fields ) { - // Add section field (maybe). - if ( ! is_int( $section_label ) ) { - $field_args = [ - 'name' => sanitize_title( $section_label ), - 'label' => $section_label, - 'type' => 'heading', - 'parent' => $parent, - 'group' => 'group/' . $parent . '/' . $group_name, - ]; + if ( + ! $restricted + && ( + 0 === $row['real_public'] + || $row['pod_object']->is_extended() + ) + ) { + $restricted = true; + } + + return $restricted; + } + + /** + * Restrict actions that can't be done for Pods with a non DB source. + * + * @since 3.1.0 + * + * @param bool $restricted Whether action is restricted. + * @param array $restrict Restriction array. + * @param string $action Current action. + * @param array $row Item data row. + * @param PodsUI $obj PodsUI object. + * + * @return bool Whether the action is restricted. + */ + public function admin_restrict_access_rights_review_restrict_dynamic_features( $restricted, $restrict, $action, $row, $obj ) { + $restricted = $this->admin_restrict_non_db_type( $restricted, $restrict, $action, $row, $obj ); - $field = new \Pods\Whatsit\Field( $field_args ); + if ( ! $restricted && 'restricted' === $row['real_restricted_dynamic_features'] ) { + $restricted = true; + } - $fields[] = $storage->add( $field ); - } + return $restricted; + } - if ( ! is_array( $section_fields ) ) { - continue; - } + /** + * Handle access rights review bulk action to set dynamic features as WP Default for a pod. + * + * @since 3.1.0 + * + * @param int[]|string[] $ids Item IDs. + * @param PodsUI $obj PodsUI object. + * + * @return false This always returns false unless there was a real error. + */ + public function admin_access_rights_review_wp_default_dynamic_features_bulk( $ids, $obj ) { + foreach ( $ids as $id ) { + if ( ! $id ) { + continue; + } - // Add fields for section. - foreach ( $section_fields as $field_name => $field_options ) { - $boolean_group = []; + $this->admin_access_rights_review_wp_default_dynamic_features( $obj, $id, 'bulk' ); + } - // Handle auto-formatting from shorthand. - if ( ! empty( $field_options['boolean_group'] ) ) { - $boolean_group = $field_options['boolean_group']; + $obj->message( __( 'Selected Pod(s) Dynamic Features set to WP Default successfully.', 'pods' ) ); - foreach ( $boolean_group as $bgf_key => $boolean_group_field ) { - // Make sure each field has a field name. - if ( is_string( $bgf_key ) ) { - $boolean_group[ $bgf_key ]['name'] = $bgf_key; - } - } + $this->update_callout( 'access_rights', false ); - $boolean_group = array_values( $boolean_group ); + return false; + } - $field_options['boolean_group'] = $boolean_group; - } + /** + * Handle access rights review bulk action to set dynamic features as WP Default for a pod. + * + * @since 3.1.0 + * + * @param PodsUI $obj PodsUI object. + * @param int|string $id Item ID. + * @param string $mode Action mode. + * + * @return mixed + */ + public function admin_access_rights_review_wp_default_dynamic_features( $obj, $id, $mode = 'single' ) { + $pod = pods_api()->load_pod( [ 'id' => $id ], false ); - if ( empty( $field_options['type'] ) ) { - continue; - } + if ( empty( $pod ) ) { + return 'bulk' !== $mode ? $obj->error( __( 'Pod not found.', 'pods' ) ) : false; + } - // Set a unique field name for boolean group headings. - if ( ! empty( $boolean_group ) ) { - $field_options['name'] = $field_name . '_' . md5( json_encode( $boolean_group ) ); - } + if ( 'post_type' !== $pod->get_object_storage_type() || $pod->is_extended() ) { + return 'bulk' !== $mode ? $obj->error( __( 'Pod cannot be modified.', 'pods' ) ) : false; + } - $field = $this->backcompat_convert_tabs_to_groups_setup_field( [ - 'field_name' => $field_name, - 'field_options' => $field_options, - 'parent' => $parent, - 'group_name' => $group_name, - ] ); + $is_public = pods_is_type_public( + [ + 'pod' => $pod, + ] + ); - $fields[] = $storage->add( $field ); - } + $options = pods_access_get_dynamic_features_allow_options(); + $value = 'inherit'; + $label = $options[ $value ] . ' - ' . ( $is_public ? $options['1'] : $options['0'] ); + + pods_api()->save_pod( [ 'id' => $id, 'dynamic_features_allow' => $value ] ); + + foreach ( $obj->data as $key => $data_pod ) { + if ( (int) $id === (int) $data_pod['id'] ) { + $obj->data[ $key ]['dynamic_features_allow'] = $label; + $obj->data[ $key ]['real_dynamic_features_allow'] = $value; } } - return compact( 'groups', 'fields' ); + if ( 'bulk' !== $mode ) { + $obj->message( __( 'Pod Dynamic Features set to WP Default successfully.', 'pods' ) ); + + $this->update_callout( 'access_rights', false ); + + $obj->manage(); + } } /** - * Setup field for backwards compatibility tabs to groups layer. - * - * @since 2.8.0 + * Restrict actions that can't be done for Pods with a non DB source. * - * @param array $args { - * The field arguments. + * @since 3.1.0 * - * @type string $field_name The field name. - * @type array $field_options The field options. - * @type string|int $parent The parent group. - * @type string $group_name The group name. - * } + * @param bool $restricted Whether action is restricted. + * @param array $restrict Restriction array. + * @param string $action Current action. + * @param array $row Item data row. + * @param PodsUI $obj PodsUI object. * - * @return \Pods\Whatsit\Field The field object. + * @return bool Whether the action is restricted. */ - public function backcompat_convert_tabs_to_groups_setup_field( $args ) { - $field_name = $args['field_name']; - $field_options = $args['field_options']; - $parent = $args['parent']; - $group_name = $args['group_name']; - - $field_args = $field_options; + public function admin_restrict_access_rights_review_wp_default_dynamic_features( $restricted, $restrict, $action, $row, $obj ) { + $restricted = $this->admin_restrict_non_db_type( $restricted, $restrict, $action, $row, $obj ); - if ( ! isset( $field_args['name'] ) ) { - $field_args['name'] = $field_name; + if ( ! $restricted && 'inherit' === $row['real_dynamic_features_allow'] ) { + $restricted = true; } - $field_args['parent'] = $parent; - $field_args['group'] = 'group/' . $parent . '/' . $group_name; + return $restricted; + } - $dfv_args = (object) [ - 'id' => 0, - 'name' => $field_args['name'], - 'value' => '', - 'pod' => null, - 'type' => pods_v( 'type', $field_args ), - 'options' => array_merge( [ - 'id' => 0, - ], $field_args ), - 'build_item_data' => true, - ]; + /** + * Handle access rights review bulk action to set dynamic features as enabled for a pod. + * + * @since 3.1.0 + * + * @param int[]|string[] $ids Item IDs. + * @param PodsUI $obj PodsUI object. + * + * @return false This always returns false unless there was a real error. + */ + public function admin_access_rights_review_enable_dynamic_features_bulk( $ids, $obj ) { + foreach ( $ids as $id ) { + if ( ! $id ) { + continue; + } - if ( ! empty( $dfv_args->type ) ) { - $field_args = PodsForm::field_method( $dfv_args->type, 'build_dfv_field_options', $field_args, $dfv_args ); + $this->admin_access_rights_review_enable_dynamic_features( $obj, $id, 'bulk' ); } - return new \Pods\Whatsit\Field( $field_args ); + $obj->message( __( 'Selected Pod(s) Dynamic Features set to Enabled successfully.', 'pods' ) ); + + $this->update_callout( 'access_rights', false ); + + return false; } /** - * Duplicate a pod + * Handle access rights review bulk action to set dynamic features as enabled for a pod. * - * @param PodsUI $obj PodsUI object. + * @since 3.1.0 + * + * @param PodsUI $obj PodsUI object. + * @param int|string $id Item ID. + * @param string $mode Action mode. + * + * @return mixed */ - public function admin_setup_duplicate( $obj ) { - $new_id = pods_api()->duplicate_pod( array( 'name' => $obj->row['name'] ) ); + public function admin_access_rights_review_enable_dynamic_features( $obj, $id, $mode = 'single' ) { + $pod = pods_api()->load_pod( [ 'id' => $id ], false ); - if ( 0 < $new_id ) { - pods_redirect( - pods_query_arg( - array( - 'action' => 'edit', - 'id' => $new_id, - 'do' => 'duplicate', - 'name' => null, - ) - ) - ); + if ( empty( $pod ) ) { + return 'bulk' !== $mode ? $obj->error( __( 'Pod not found.', 'pods' ) ) : false; + } - return; + if ( 'post_type' !== $pod->get_object_storage_type() || $pod->is_extended() ) { + return 'bulk' !== $mode ? $obj->error( __( 'Pod cannot be modified.', 'pods' ) ) : false; } - $obj->error( __( 'An error occurred, the Pod was not duplicated.', 'pods' ) ); + $options = pods_access_get_dynamic_features_allow_options(); + $value = '1'; + $label = $options[ $value ]; + + pods_api()->save_pod( [ 'id' => $id, 'dynamic_features_allow' => $value ] ); + + foreach ( $obj->data as $key => $data_pod ) { + if ( (int) $id === (int) $data_pod['id'] ) { + $obj->data[ $key ]['dynamic_features_allow'] = $label; + $obj->data[ $key ]['real_dynamic_features_allow'] = $value; + } + } + + if ( 'bulk' !== $mode ) { + $obj->message( __( 'Pod Dynamic Features set to Enabled successfully.', 'pods' ) ); + + $this->update_callout( 'access_rights', false ); + + $obj->manage(); + } } /** - * Restrict Duplicate action to custom types, not extended + * Restrict actions that can't be done for Pods with a non DB source. + * + * @since 3.1.0 * * @param bool $restricted Whether action is restricted. * @param array $restrict Restriction array. @@ -2086,18 +3329,12 @@ public function admin_setup_duplicate( $obj ) { * @param array $row Item data row. * @param PodsUI $obj PodsUI object. * - * @since 2.3.10 - * - * @return bool + * @return bool Whether the action is restricted. */ - public function admin_setup_duplicate_restrict( $restricted, $restrict, $action, $row, $obj ) { - if ( in_array( - $row['real_type'], array( - 'user', - 'media', - 'comment', - ), true - ) ) { + public function admin_restrict_access_rights_review_enable_dynamic_features( $restricted, $restrict, $action, $row, $obj ) { + $restricted = $this->admin_restrict_non_db_type( $restricted, $restrict, $action, $row, $obj ); + + if ( ! $restricted && '1' === $row['real_dynamic_features_allow'] ) { $restricted = true; } @@ -2105,39 +3342,79 @@ public function admin_setup_duplicate_restrict( $restricted, $restrict, $action, } /** - * Delete a pod + * Handle access rights review bulk action to set dynamic features as disabled for a pod. * - * @param PodsUI $obj PodsUI object. - * @param int|string $id Item ID. + * @since 3.1.0 * - * @return mixed + * @param int[]|string[] $ids Item IDs. + * @param PodsUI $obj PodsUI object. + * + * @return false This always returns false unless there was a real error. */ - public function admin_setup_delete( $obj, $id ) { + public function admin_access_rights_review_disable_dynamic_features_bulk( $ids, $obj ) { + foreach ( $ids as $id ) { + if ( ! $id ) { + continue; + } - $pod = pods_api()->load_pod( array( 'id' => $id ), false ); + $this->admin_access_rights_review_disable_dynamic_features( $obj, $id, 'bulk' ); + } + + $obj->message( __( 'Selected Pod(s) Dynamic Features set to Disabled successfully.', 'pods' ) ); + + $this->update_callout( 'access_rights', false ); + + return false; + } + + /** + * Handle access rights review bulk action to set dynamic features as disabled for a pod. + * + * @since 3.1.0 + * + * @param PodsUI $obj PodsUI object. + * @param int|string $id Item ID. + * @param string $mode Action mode. + * + * @return mixed + */ + public function admin_access_rights_review_disable_dynamic_features( $obj, $id, $mode = 'single' ) { + $pod = pods_api()->load_pod( [ 'id' => $id ], false ); if ( empty( $pod ) ) { - return $obj->error( __( 'Pod not found.', 'pods' ) ); + return 'bulk' !== $mode ? $obj->error( __( 'Pod not found.', 'pods' ) ) : false; } - pods_api()->delete_pod( array( 'id' => $id ) ); + if ( 'post_type' !== $pod->get_object_storage_type() || $pod->is_extended() ) { + return 'bulk' !== $mode ? $obj->error( __( 'Pod cannot be modified.', 'pods' ) ) : false; + } + + $options = pods_access_get_dynamic_features_allow_options(); + $value = '0'; + $label = $options[ $value ]; + + pods_api()->save_pod( [ 'id' => $id, 'dynamic_features_allow' => $value ] ); foreach ( $obj->data as $key => $data_pod ) { if ( (int) $id === (int) $data_pod['id'] ) { - unset( $obj->data[ $key ] ); + $obj->data[ $key ]['dynamic_features_allow'] = $label; + $obj->data[ $key ]['real_dynamic_features_allow'] = $value; } } - $obj->total = count( $obj->data ); - $obj->total_found = count( $obj->data ); + if ( 'bulk' !== $mode ) { + $obj->message( __( 'Pod Dynamic Features set to Disabled successfully.', 'pods' ) ); - $obj->message( __( 'Pod deleted successfully.', 'pods' ) ); + $this->update_callout( 'access_rights', false ); - $obj->manage(); + $obj->manage(); + } } /** - * Restrict Delete action. + * Restrict actions that can't be done for Pods with a non DB source. + * + * @since 3.1.0 * * @param bool $restricted Whether action is restricted. * @param array $restrict Restriction array. @@ -2145,12 +3422,35 @@ public function admin_setup_delete( $obj, $id ) { * @param array $row Item data row. * @param PodsUI $obj PodsUI object. * - * @since 2.3.10 + * @return bool Whether the action is restricted. + */ + public function admin_restrict_access_rights_review_disable_dynamic_features( $restricted, $restrict, $action, $row, $obj ) { + $restricted = $this->admin_restrict_non_db_type( $restricted, $restrict, $action, $row, $obj ); + + if ( ! $restricted && '0' === $row['real_dynamic_features_allow'] ) { + $restricted = true; + } + + return $restricted; + } + + /** + * Restrict actions that can't be done for Pods with a non DB source. * - * @return bool + * @since 3.1.0 + * + * @param bool $restricted Whether action is restricted. + * @param array $restrict Restriction array. + * @param string $action Current action. + * @param array $row Item data row. + * @param PodsUI $obj PodsUI object. + * + * @return bool Whether the action is restricted. */ - public function admin_setup_delete_restrict( $restricted, $restrict, $action, $row, $obj ) { - if ( __( 'DB', 'pods' ) !== $row['source'] ) { + public function admin_restrict_access_rights_review_unrestrict_dynamic_features( $restricted, $restrict, $action, $row, $obj ) { + $restricted = $this->admin_restrict_non_db_type( $restricted, $restrict, $action, $row, $obj ); + + if ( ! $restricted && 'unrestricted' === $row['real_restricted_dynamic_features'] ) { $restricted = true; } @@ -2180,7 +3480,11 @@ public function admin_settings() { do_action( 'pods_admin_settings_init' ); // Add our custom callouts. - add_action( 'pods_admin_after_settings', array( $this, 'admin_manage_callouts' ) ); + if ( $this->has_horizontal_callout() ) { + add_action( 'pods_admin_before_settings', [ $this, 'admin_manage_callouts' ] ); + } else { + add_action( 'pods_admin_after_settings', [ $this, 'admin_manage_callouts' ] ); + } pods_view( PODS_DIR . 'ui/admin/settings.php', compact( array_keys( get_defined_vars() ) ) ); } @@ -2407,7 +3711,12 @@ public function admin_components() { $this->handle_callouts_updates(); add_filter( 'pods_ui_manage_custom_container_classes', array( $this, 'admin_manage_container_class' ) ); - add_action( 'pods_ui_manage_after_container', array( $this, 'admin_manage_callouts' ) ); + + if ( $this->has_horizontal_callout() ) { + add_action( 'pods_ui_manage_before_container', [ $this, 'admin_manage_callouts' ] ); + } else { + add_action( 'pods_ui_manage_after_container', [ $this, 'admin_manage_callouts' ] ); + } pods_ui( $ui ); } @@ -2555,7 +3864,11 @@ public function admin_help() { // Add our custom callouts. $this->handle_callouts_updates(); - add_action( 'pods_admin_after_help', array( $this, 'admin_manage_callouts' ) ); + if ( $this->has_horizontal_callout() ) { + add_action( 'pods_admin_before_help', [ $this, 'admin_manage_callouts' ] ); + } else { + add_action( 'pods_admin_after_help', [ $this, 'admin_manage_callouts' ] ); + } pods_view( PODS_DIR . 'ui/admin/help.php', compact( array_keys( get_defined_vars() ) ) ); } @@ -2762,7 +4075,14 @@ public function admin_ajax() { $method = (object) array_merge( $defaults, (array) $methods[ $params->method ] ); - if ( true !== $method->custom_nonce && ( ! isset( $params->_wpnonce ) || false === wp_verify_nonce( $params->_wpnonce, 'pods-' . $params->method ) ) ) { + if ( + true !== $method->custom_nonce + && ( + ! is_user_logged_in() + || ! isset( $params->_wpnonce ) + || false === wp_verify_nonce( $params->_wpnonce, 'pods-' . $params->method ) + ) + ) { pods_error( __( 'Unauthorized request', 'pods' ), $this ); } @@ -2775,8 +4095,13 @@ public function admin_ajax() { } // Check permissions (convert to array to support multiple) - if ( ! empty( $method->priv ) && ! pods_is_admin( array( 'pods' ) ) && true !== $method->priv && ! pods_is_admin( $method->priv ) ) { - pods_error( __( 'Access denied', 'pods' ), $this ); + if ( ! empty( $method->priv ) && ! pods_is_admin( array( 'pods' ) ) ) { + if ( true !== $method->priv && pods_is_admin( $method->priv ) ) { + // They have access to the custom priv. + } else { + // They do not have access. + pods_error( __( 'Access denied', 'pods' ), $this ); + } } $params->method = $method->name; @@ -3202,11 +4527,11 @@ public function add_debug_information( $info ) { $settings = pods_container( Settings::class ); - $fields = $settings->get_setting_fields(); + $settings_fields = $settings->get_setting_fields(); $settings_values = $settings->get_settings(); - $auto_start = pods_v( $auto_start, $fields['session_auto_start']['data'], __( 'Unknown', 'pods' ) ); + $auto_start = pods_v( $auto_start, $settings_fields['session_auto_start']['data'], __( 'Unknown', 'pods' ) ); global $wpdb; @@ -3214,6 +4539,10 @@ public function add_debug_information( $info ) { 'label' => 'Pods', 'description' => __( 'Debug information for Pods installations.', 'pods' ), 'fields' => [ + 'pods-version' => [ + 'label' => __( 'Pods Version', 'pods' ), + 'value' => PODS_VERSION, + ], 'pods-server-software' => [ 'label' => __( 'Server Software', 'pods' ), 'value' => ! empty( $_SERVER['SERVER_SOFTWARE'] ) ? $_SERVER['SERVER_SOFTWARE'] : 'N/A', @@ -3326,10 +4655,6 @@ public function add_debug_information( $info ) { 'label' => __( 'Pods Shortcode Allow Evaluate Tags' ), 'value' => ( pods_shortcode_allow_evaluate_tags() ) ? __( 'Yes', 'pods' ) : __( 'No', 'pods' ), ], - 'pods-sessions' => [ - 'label' => __( 'Pods Sessions' ), - 'value' => $auto_start, - ], 'pods-can-use-sessions' => [ 'label' => __( 'Pods Can Use Sessions' ), 'value' => ( pods_can_use_sessions( true ) ) ? __( 'Yes', 'pods' ) : __( 'No', 'pods' ), @@ -3337,34 +4662,56 @@ public function add_debug_information( $info ) { ], ]; - $settings_to_show = [ - 'types_only' => __( 'Setting: Types only', 'pods' ), - 'watch_changed_fields' => __( 'Setting: Watch Changed fields', 'pods' ), - 'metadata_integration' => __( 'Setting: Watch WP Metadata calls', 'pods' ), - 'metadata_override_get' => __( 'Setting: Override WP Metadata values', 'pods' ), - ]; + foreach ( $settings_fields as $setting_name => $setting_field ) { + if ( empty( $setting_field['site_health_include_in_info'] ) ) { + continue; + } + + if ( isset( $setting_field['name'] ) ) { + $setting_name = $setting_field['name']; + } - foreach ( $settings_to_show as $setting => $label ) { - $setting_key = 'pods-settings-' . sanitize_title_with_dashes( $setting ); + $setting_key = 'pods-settings-' . sanitize_title_with_dashes( $setting_name ); - $value = ucwords( - str_replace( - [ '_', '-' ], - ' ', - (string) pods_v( $setting, $settings_values, __( 'Unknown', 'pods' ) - ) - ) - ); + $value = pods_v( $setting_name, $settings_values ); + + $original_value = is_array( $value ) ? implode( ',', $value ) : (string) $value; + + if ( is_array( $value ) ) { + foreach ( $value as $k => $v ) { + $v = (string) $v; + + $has_v = 0 < strlen( $v ); + + if ( $has_v && isset( $setting_field['site_health_data'] ) && isset( $setting_field['site_health_data'][ $v ] ) ) { + $value[ $k ] = $setting_field['site_health_data'][ $v ]; + } elseif ( $has_v && isset( $setting_field['data'] ) && isset( $setting_field['data'][ $v ] ) ) { + $value[ $k ] = $setting_field['data'][ $v ]; + } + } + + $value = pods_serial_comma( $value ); + } else { + $value = (string) $value; + + $has_value = 0 < strlen( $value ); + + if ( $has_value && isset( $setting_field['site_health_data'] ) && isset( $setting_field['site_health_data'][ $value ] ) ) { + $value = $setting_field['site_health_data'][ $value ]; + } elseif ( $has_value && isset( $setting_field['data'] ) && isset( $setting_field['data'][ $value ] ) ) { + $value = $setting_field['data'][ $value ]; + } elseif ( 'boolean' === $setting_field['data'] || '1' === $value || '0' === $value ) { + $value = '1' === $value ? __( 'Yes', 'pods' ) : __( 'No', 'pods' ); + } + } - if ( '0' === $value ) { - $value = __( 'No', 'pods' ); - } elseif ( '1' === $value ) { - $value = __( 'Yes', 'pods' ); + if ( 'unknown' === $value || '' === $value ) { + $value = __( 'Unknown', 'pods' ); } $info['pods']['fields'][ $setting_key ] = [ - 'label' => $label, - 'value' => $value, + 'label' => $setting_field['label'], + 'value' => $value . ( $value !== $original_value ? ' [' . $setting_name . '=' . $original_value . ']' : '' ), ]; } diff --git a/classes/PodsData.php b/classes/PodsData.php index 4c3fc72a40..258255b43d 100644 --- a/classes/PodsData.php +++ b/classes/PodsData.php @@ -714,6 +714,25 @@ public function select( $params ) { */ $results = apply_filters( 'pods_data_select', $results, $params, $this ); + // Clean up data we don't want to work with. + if ( + ( + $this->pod_data + && 'user' === $this->pod_data->get_type() + ) + || $wpdb->users === $this->table + ) { + $results = pods_access_bleep_items( $results ); + } elseif ( + ( + $this->pod_data + && 'post_type' === $this->pod_data->get_type() + ) + || $wpdb->posts === $this->table + ) { + $results = pods_access_bleep_items( $results ); + } + $this->rows = $results; $this->row_number = - 1; @@ -2147,6 +2166,8 @@ public function fetch( $row = null, $explicit_set = true ) { $this->row = false; } else { $current_row_id = (int) $this->row['ID']; + + $this->row = pods_access_bleep_data( $this->row ); } $get_table_data = true; @@ -2223,7 +2244,7 @@ public function fetch( $row = null, $explicit_set = true ) { $this->row['caps'] = $caps; $this->row['allcaps'] = $allcaps; - unset( $this->row['user_pass'] ); + $this->row = pods_access_bleep_data( $this->row ); $current_row_id = (int) $this->row['ID']; } @@ -2331,6 +2352,8 @@ public function fetch( $row = null, $explicit_set = true ) { } }//end if + $this->row = pods_access_bleep_data( $this->row ); + $this->row = apply_filters( 'pods_data_fetch', $this->row, $id, $this->row_number, $this ); // Set the ID if the row was found. diff --git a/classes/PodsField.php b/classes/PodsField.php index 69245aa899..f593a488b6 100644 --- a/classes/PodsField.php +++ b/classes/PodsField.php @@ -288,9 +288,7 @@ public function value( $value = null, $name = null, $options = null, $pod = null * @since 2.0.0 */ public function display( $value = null, $name = null, $options = null, $pod = null, $id = null ) { - - return $value; - + return $this->maybe_sanitize_output( $value, $options ); } /** @@ -920,11 +918,47 @@ public function strip_html( $value, $options = null ) { } } - return $value; + return $this->maybe_sanitize_output( $value, $options ); } } - return strip_tags( $value ); + return wp_strip_all_tags( $value ); + } + + /** + * Determine whether the field value needs to be sanitized and sanitize it. + * + * @since 3.1.0 + * + * @param mixed $value The field value. + * @param null|array|Field $options The field options. + * + * @return mixed The sanitized field value if it needs to be sanitized. + */ + public function maybe_sanitize_output( $value, $options = null ) { + // Maybe check for a sanitize output option. + $should_sanitize = null === $options || 1 === (int) pods_v( 'sanitize_output', $options, 1 ); + + /** + * Allow filtering whether to sanitize the field value before output. + * + * @since 3.1.0 + * + * @param bool $should_sanitize Whether the field value should be sanitized. + * @param mixed $value The field value. + * @param null|array|Field $options The field options. + */ + $should_sanitize = apply_filters( 'pods_field_maybe_sanitize_output', $should_sanitize, $value, $options ); + + if ( $should_sanitize ) { + if ( is_string( $value ) ) { + $value = wp_kses_post( $value ); + } elseif ( is_array( $value ) || is_object( $value ) ) { + $value = wp_kses_post_deep( $value ); + } + } + + return $value; } /** diff --git a/classes/PodsInit.php b/classes/PodsInit.php index 7c1bc84bb5..3dc0941c93 100644 --- a/classes/PodsInit.php +++ b/classes/PodsInit.php @@ -393,9 +393,9 @@ public function stats_tracking( $plugin_file, $plugin_slug ) { add_filter( 'wisdom_notice_text_' . $plugin_slug, static function() { return - __( 'Thank you for installing our plugin. We\'d like your permission to track its usage on your site to make improvements to the plugin and provide better support when you reach out. We won\'t record any sensitive data -- we will only gather information regarding the WordPress environment, your site admin email address, and plugin settings.', 'pods' ) + __( 'Thank you for installing our plugin. We\'d like your permission to track its usage on your site to make improvements to the plugin and provide better support when you reach out. We won\'t record any sensitive data -- we will only gather information regarding the WordPress environment, your site admin email address, and plugin settings. Tracking is completely optional.', 'pods' ) . "\n\n" - . __( 'Any information collected is not shared with third-parties and you will not be signed up for mailing lists. Tracking is completely optional.', 'pods' ); + . __( 'Any information collected is not shared with third-parties and you will not be signed up for mailing lists.', 'pods' ); } ); // Handle non-Pods pages, we don't want certain things happening. @@ -1463,6 +1463,7 @@ public function setup_content_types( $force = false ) { 'labels' => $ct_labels, 'description' => esc_html( pods_v( 'description', $taxonomy ) ), 'public' => (boolean) pods_v( 'public', $taxonomy, true ), + 'publicly_queryable' => (boolean) pods_v( 'publicly_queryable', $taxonomy, (boolean) pods_v( 'public', $taxonomy, true ) ), 'show_ui' => (boolean) pods_v( 'show_ui', $taxonomy, (boolean) pods_v( 'public', $taxonomy, true ) ), 'show_in_menu' => (boolean) pods_v( 'show_in_menu', $taxonomy, (boolean) pods_v( 'public', $taxonomy, true ) ), 'show_in_nav_menus' => (boolean) pods_v( 'show_in_nav_menus', $taxonomy, (boolean) pods_v( 'public', $taxonomy, true ) ), @@ -2610,14 +2611,24 @@ public function delete_attachment( $_ID ) { * Register widgets for Pods */ public function register_widgets() { + $widgets = []; - $widgets = array( - 'PodsWidgetSingle', - 'PodsWidgetList', - 'PodsWidgetField', - 'PodsWidgetForm', - 'PodsWidgetView', - ); + // Maybe register the display widgets. + if ( pods_can_use_dynamic_feature( 'display' ) ) { + $widgets[] = 'PodsWidgetSingle'; + $widgets[] = 'PodsWidgetList'; + $widgets[] = 'PodsWidgetField'; + } + + // Maybe register the form widget. + if ( pods_can_use_dynamic_feature( 'form' ) ) { + $widgets[] = 'PodsWidgetForm'; + } + + // Maybe register the view widget. + if ( pods_can_use_dynamic_feature( 'view' ) ) { + $widgets[] = 'PodsWidgetView'; + } foreach ( $widgets as $widget ) { if ( ! file_exists( PODS_DIR . 'classes/widgets/' . $widget . '.php' ) ) { diff --git a/classes/PodsMeta.php b/classes/PodsMeta.php index 4cf66c2aa0..9bc620f208 100644 --- a/classes/PodsMeta.php +++ b/classes/PodsMeta.php @@ -3830,17 +3830,14 @@ public function get_object( $object_type, $object_id, $aux = '' ) { * @return array|bool|int|mixed|null|string|void */ public function get_meta( $object_type, $_null = null, $object_id = 0, $meta_key = '', $single = false ) { - $metadata_integration = (int) pods_get_setting( 'metadata_integration', 1 ); + $metadata_integration = (int) pods_get_setting( 'metadata_integration' ); // Only continue if metadata is integrated with. if ( 0 === $metadata_integration ) { return $_null; } - $first_pods_version = get_option( 'pods_framework_version_first' ); - $first_pods_version = '' === $first_pods_version ? PODS_VERSION : $first_pods_version; - - $metadata_override_get = (int) pods_get_setting( 'metadata_override_get', version_compare( $first_pods_version, '2.8.21', '<=' ) ? 1 : 0 ); + $metadata_override_get = (int) pods_get_setting( 'metadata_override_get' ); // Only continue if metadata is overridden. if ( 0 === $metadata_override_get ) { @@ -4085,7 +4082,7 @@ public function add_meta( $object_type, $_null = null, $object_id = 0, $meta_key return $_null; } - $metadata_integration = (int) pods_get_setting( 'metadata_integration', 1 ); + $metadata_integration = (int) pods_get_setting( 'metadata_integration' ); // Only continue if metadata is integrated with. if ( 0 === $metadata_integration ) { @@ -4206,7 +4203,7 @@ public function update_meta( $object_type, $_null = null, $object_id = 0, $meta_ return $_null; } - $metadata_integration = (int) pods_get_setting( 'metadata_integration', 1 ); + $metadata_integration = (int) pods_get_setting( 'metadata_integration' ); // Only continue if metadata is integrated with. if ( 0 === $metadata_integration ) { @@ -4329,7 +4326,7 @@ public function update_meta( $object_type, $_null = null, $object_id = 0, $meta_ * @return bool|int|null */ public function update_meta_by_id( $object_type, $_null = null, $meta_id = 0, $meta_key = '', $meta_value = '' ) { - $metadata_integration = (int) pods_get_setting( 'metadata_integration', 1 ); + $metadata_integration = (int) pods_get_setting( 'metadata_integration' ); // Only continue if metadata is integrated with. if ( 0 === $metadata_integration ) { @@ -4369,7 +4366,7 @@ public function delete_meta( $object_type, $_null = null, $object_id = 0, $meta_ return $_null; } - $metadata_integration = (int) pods_get_setting( 'metadata_integration', 1 ); + $metadata_integration = (int) pods_get_setting( 'metadata_integration' ); // Only continue if metadata is integrated with. if ( 0 === $metadata_integration ) { @@ -4488,7 +4485,7 @@ public function delete_meta( $object_type, $_null = null, $object_id = 0, $meta_ * @return bool|int|null */ public function delete_meta_by_id( $object_type, $_null = null, $meta_id = 0 ) { - $metadata_integration = (int) pods_get_setting( 'metadata_integration', 1 ); + $metadata_integration = (int) pods_get_setting( 'metadata_integration' ); // Only continue if metadata is integrated with. if ( 0 === $metadata_integration ) { diff --git a/classes/PodsUI.php b/classes/PodsUI.php index b4c86c8246..95ee27c1fd 100644 --- a/classes/PodsUI.php +++ b/classes/PodsUI.php @@ -1242,11 +1242,8 @@ public function message( $msg, $error = false ) { if ( empty( $msg ) ) { return; } - ?> -
-

-
- save(); } $this->edit( ( 'duplicate' === $this->action && ! in_array( $this->action, $this->actions_disabled ) ) ? true : false ); - } elseif ( 'delete' === $this->action && ! in_array( $this->action, $this->actions_disabled ) && false !== wp_verify_nonce( $this->_nonce, 'pods-ui-action-delete' ) ) { + } elseif ( 'delete' === $this->action && ! in_array( $this->action, $this->actions_disabled ) && false !== wp_verify_nonce( $this->_nonce, 'pods-ui-action-delete-' . $this->id ) ) { $this->delete( $this->id ); $this->manage(); } elseif ( 'reorder' === $this->action && ! in_array( $this->action, $this->actions_disabled ) && false !== $this->reorder['on'] ) { @@ -1344,7 +1341,7 @@ public function go() { $this->restricted( $this->action, $row ) || ( $use_nonce - && false === wp_verify_nonce( $this->_nonce, 'pods-ui-action-' . $this->action ) + && false === wp_verify_nonce( $this->_nonce, 'pods-ui-action-' . $this->action . '-' . $this->id ) ) ) { return $this->error( sprintf( __( 'Error: You do not have access to this %s.', 'pods' ), $this->item ) ); @@ -2652,6 +2649,15 @@ public function manage( $reorder = false ) { $custom_container_classes = implode( ' ', $custom_container_classes ); ?>
+ +
actions_disabled, true ) && ! in_array( 'manage_header', $this->actions_hidden, true ) ) : ?>
> @@ -2713,6 +2719,15 @@ public function manage( $reorder = false ) { + +
view == $view ) { - $label = '' . esc_html( $label ) . ''; + $label = '' . wp_kses_post( $label ) . ''; } else { - $label = '' . esc_html( $label ) . ''; + $label = '' . wp_kses_post( $label ) . ''; } } else { $label = wp_kses_post( $label ); } ?> -
  • +
  • error( __( 'Error: Invalid Configuration - Missing "fields" definition.', 'pods' ) ); } ?> - reorder ) ? ' id="admin_ui_reorder"' : ''; ?>> +
    reorder ) ? ' id="admin_ui_reorder"' : ''; ?>> actions_bulk ) ) { ?> do_template( $this->action_links['view'], $row ); } - $actions['view'] = '' . __( 'View', 'pods' ) . ''; + $title = esc_html__( 'View this item', 'pods' ); + $label = esc_html__( 'View', 'pods' ); + + if ( ! empty( $this->actions_custom['view'] ) ) { + if ( ! empty( $this->actions_custom['view']['label'] ) ) { + $label = $this->do_template( $this->actions_custom['view']['label'], $row ); + } + + if ( ! empty( $this->actions_custom['view']['title'] ) ) { + $title = $this->do_template( $this->actions_custom['view']['title'], $row ); + } + } + + $actions['view'] = '' . $label . ''; } if ( ! in_array( 'edit', $this->actions_disabled ) && ! in_array( 'edit', $this->actions_hidden ) && ! $this->restricted( 'edit', $row ) ) { @@ -4427,7 +4455,20 @@ public function get_actions( $row ) { $link = $this->do_template( $this->action_links['edit'], $row ); } - $actions['edit'] = '' . __( 'Edit', 'pods' ) . ''; + $title = esc_html__( 'Edit this item', 'pods' ); + $label = esc_html__( 'Edit', 'pods' ); + + if ( ! empty( $this->actions_custom['edit'] ) ) { + if ( ! empty( $this->actions_custom['edit']['label'] ) ) { + $label = $this->do_template( $this->actions_custom['edit']['label'], $row ); + } + + if ( ! empty( $this->actions_custom['edit']['title'] ) ) { + $title = $this->do_template( $this->actions_custom['edit']['title'], $row ); + } + } + + $actions['edit'] = '' . $label . ''; } if ( ! in_array( 'duplicate', $this->actions_disabled ) && ! in_array( 'duplicate', $this->actions_hidden ) && ! $this->restricted( 'edit', $row ) ) { @@ -4442,7 +4483,20 @@ public function get_actions( $row ) { $link = $this->do_template( $this->action_links['duplicate'], $row ); } - $actions['duplicate'] = '' . __( 'Duplicate', 'pods' ) . ''; + $title = esc_html__( 'Duplicate this item', 'pods' ); + $label = esc_html__( 'Duplicate', 'pods' ); + + if ( ! empty( $this->actions_custom['duplicate'] ) ) { + if ( ! empty( $this->actions_custom['duplicate']['label'] ) ) { + $label = $this->do_template( $this->actions_custom['duplicate']['label'], $row ); + } + + if ( ! empty( $this->actions_custom['duplicate']['title'] ) ) { + $title = $this->do_template( $this->actions_custom['duplicate']['title'], $row ); + } + } + + $actions['duplicate'] = '' . $label . ''; } if ( ! in_array( 'delete', $this->actions_disabled ) && ! in_array( 'delete', $this->actions_hidden ) && ! $this->restricted( 'delete', $row ) ) { @@ -4450,12 +4504,12 @@ public function get_actions( $row ) { array( $this->num_prefix . 'action' . $this->num => 'delete', $this->num_prefix . 'id' . $this->num => $field_id, - $this->num_prefix . '_wpnonce' . $this->num => wp_create_nonce( 'pods-ui-action-delete' ), + $this->num_prefix . '_wpnonce' . $this->num => wp_create_nonce( 'pods-ui-action-delete-' . $field_id ), ), self::$allowed, $this->exclusion() ); if ( ! empty( $this->action_links['delete'] ) ) { - $link = add_query_arg( array( $this->num_prefix . '_wpnonce' . $this->num => wp_create_nonce( 'pods-ui-action-delete' ) ), $this->do_template( $this->action_links['delete'], $row ) ); + $link = add_query_arg( array( $this->num_prefix . '_wpnonce' . $this->num => wp_create_nonce( 'pods-ui-action-delete-' . $field_id ) ), $this->do_template( $this->action_links['delete'], $row ) ); } $actions['delete'] = '' . __( 'Delete', 'pods' ) . ''; @@ -4496,7 +4550,7 @@ public function get_actions( $row ) { $vars = array( $this->num_prefix . 'action' . $this->num => $custom_action, $this->num_prefix . 'id' . $this->num => $field_id, - $this->num_prefix . '_wpnonce' . $this->num => wp_create_nonce( 'pods-ui-action-' . $custom_action ), + $this->num_prefix . '_wpnonce' . $this->num => wp_create_nonce( 'pods-ui-action-' . $custom_action . '-' . $field_id ), ); if ( 'toggle' === $custom_action ) { @@ -4507,7 +4561,7 @@ public function get_actions( $row ) { $custom_data['link'] = pods_query_arg( $vars, self::$allowed, $this->exclusion() ); if ( isset( $this->action_links[ $custom_action ] ) && ! empty( $this->action_links[ $custom_action ] ) ) { - $custom_data['link'] = add_query_arg( array( $this->num_prefix . '_wpnonce' . $this->num => wp_create_nonce( 'pods-ui-action-' . $custom_action ) ), $this->do_template( $this->action_links[ $custom_action ], $row ) ); + $custom_data['link'] = add_query_arg( array( $this->num_prefix . '_wpnonce' . $this->num => wp_create_nonce( 'pods-ui-action-' . $custom_action . '-' . $field_id ) ), $this->do_template( $this->action_links[ $custom_action ], $row ) ); } } diff --git a/classes/PodsView.php b/classes/PodsView.php index 6f42cc3e6c..c22f6bd20c 100644 --- a/classes/PodsView.php +++ b/classes/PodsView.php @@ -159,27 +159,27 @@ public static function reset_cached_keys( $cache_mode = null, $group = null ) { * @param array|null $data (optional) Data to pass on to the template * @param bool|int|array $expires (optional) Time in seconds for the cache to expire, if 0 no expiration. * @param string $cache_mode (optional) Decides the caching method to use for the view. + * @param bool $limited (optional) Whether to limit the view to only the theme directory, defaults to false * * @return bool|mixed|null|string|void * * @since 2.0.0 */ - public static function view( $view, $data = null, $expires = false, $cache_mode = 'cache' ) { + public static function view( $view, $data = null, $expires = false, $cache_mode = 'cache', $limited = false ) { /** - * Override the value of $view. For example, using Pods AJAX View. - * - * To use, set first param to true. If that param in not null, this method returns its value. - * - * @param null|bool If not set to null, this filter overrides the rest of the method. - * @param string $view Path of the view file - * @param array|null $data (optional) Data to pass on to the template - * @param bool|int|array $expires (optional) Time in seconds for the cache to expire, if 0 no expiration. - * @param string $cache_mode (optional) Decides the caching method to use for the view. + * Allow filtering the view before the logic runs. * * @since 2.4.1 + * + * @param null|false|string $filter_check The filter check. If not set to null, return the value as the output. Set to false to fail to load the view. + * @param string $view Path of the view file. + * @param array|null $data Data to pass on to the template. + * @param bool|int|array $expires Time in seconds for the cache to expire, if 0 no expiration. + * @param string $cache_mode Decides the caching method to use for the view. + * @param bool $limited Whether to limit the view to only the theme directory, defaults to false. */ - $filter_check = apply_filters( 'pods_view_alt_view', null, $view, $data, $expires, $cache_mode ); + $filter_check = apply_filters( 'pods_view_alt_view', null, $view, $data, $expires, $cache_mode, $limited ); if ( null !== $filter_check ) { return $filter_check; @@ -216,7 +216,18 @@ public static function view( $view, $data = null, $expires = false, $cache_mode $view_id = pods_evaluate_tags( $view_id ); } - $view = apply_filters( 'pods_view_inc', $view, $data, $expires, $cache_mode ); + /** + * Allow filtering the path of the view to use. + * + * @since unknown + * + * @param string $view Path of the view file. + * @param array|null $data Data to pass on to the template. + * @param bool|int|array $expires Time in seconds for the cache to expire, if 0 no expiration. + * @param string $cache_mode Decides the caching method to use for the view. + * @param bool $limited Whether to limit the view to only the theme directory, defaults to false. + */ + $view = apply_filters( 'pods_view_inc', $view, $data, $expires, $cache_mode, $limited ); $view_key = $view; @@ -254,12 +265,84 @@ public static function view( $view, $data = null, $expires = false, $cache_mode self::set( 'pods-view-' . $cache_key . $view_id, $output, $expires, $cache_mode, 'pods_view' ); } - $output = apply_filters( "pods_view_output_{$cache_key}", $output, $view, $data, $expires, $cache_mode ); - $output = apply_filters( 'pods_view_output', $output, $view, $data, $expires, $cache_mode ); + /** + * Allow filtering the path of the view to use based on the cache key. + * + * @since unknown + * + * @param string|false $output The view output. Returns as false if the view fails to load. + * @param string $view Path of the view file. + * @param array|null $data Data to pass on to the template. + * @param bool|int|array $expires Time in seconds for the cache to expire, if 0 no expiration. + * @param string $cache_mode Decides the caching method to use for the view. + * @param bool $limited Whether to limit the view to only the theme directory, defaults to false. + */ + $output = apply_filters( "pods_view_output_{$cache_key}", $output, $view, $data, $expires, $cache_mode, $limited ); + + /** + * Allow filtering the path of the view to use. + * + * @since unknown + * + * @param string|false $output The view output. Returns as false if the view fails to load. + * @param string $view Path of the view file. + * @param array|null $data Data to pass on to the template. + * @param bool|int|array $expires Time in seconds for the cache to expire, if 0 no expiration. + * @param string $cache_mode Decides the caching method to use for the view. + * @param bool $limited Whether to limit the view to only the theme directory, defaults to false. + */ + $output = apply_filters( 'pods_view_output', $output, $view, $data, $expires, $cache_mode, $limited ); return $output; } + /** + * Get the full path of the view if it exists. + * + * @since 3.1.0 + * + * @param string $view Path of the view file + * @param bool $limited (optional) Whether to limit the view to only the theme directory, defaults to false + * + * @return string|false The full path of the view if it exists. + */ + public static function view_get_path( $view, $limited = false ) { + // Support my-view.php?custom-key=X#hash keying for cache + if ( ! is_array( $view ) ) { + $view_q = explode( '?', $view ); + + if ( 1 < count( $view_q ) ) { + $view = $view_q[0]; + } + + $view_h = explode( '#', $view ); + + if ( 1 < count( $view_h ) ) { + $view = $view_h[0]; + } + } + + $view = apply_filters( 'pods_view_inc', $view, null, false, 'cache', $limited ); + + $view_key = $view; + + if ( is_array( $view_key ) ) { + $view_key = implode( '-', $view_key ) . '.php'; + } + + if ( false !== realpath( $view_key ) ) { + $view_key = realpath( $view_key ); + } + + $view_path = self::locate_template( $view_key, $limited ); + + if ( empty( $view_path ) ) { + return false; + } + + return $view_path; + } + /** * Get the cache key, salted with current Pods version, peppered with md5 if too long * @@ -703,10 +786,11 @@ public static function clear( $key = true, $cache_mode = null, $group = '' ) { * * @param $_view * @param null|array $_data + * @param bool $limited (optional) Whether to limit the view to only the theme directory, defaults to false * * @return bool|mixed|string|void */ - public static function get_template_part( $_view, $_data = null ) { + public static function get_template_part( $_view, $_data = null, $limited = false ) { /* To be reviewed later, should have more checks and restrictions like a whitelist etc. @@ -723,7 +807,7 @@ public static function get_template_part( $_view, $_data = null ) { } */ - $_view = self::locate_template( $_view ); + $_view = self::locate_template( $_view, $limited ); if ( empty( $_view ) ) { return $_view; @@ -743,11 +827,12 @@ public static function get_template_part( $_view, $_data = null ) { /** * @static * - * @param $_view + * @param array|string $_view + * @param bool $limited (optional) Whether to limit the view to only the theme directory, defaults to false * * @return bool|mixed|string|void */ - private static function locate_template( $_view ) { + private static function locate_template( $_view, $limited = false ) { if ( is_array( $_view ) ) { $_views = []; @@ -776,9 +861,15 @@ private static function locate_template( $_view ) { return $_view; }//end if + $paths_to_check = [ 'plugins', 'pods', 'theme' ]; + + if ( $limited ) { + $paths_to_check = [ 'theme' ]; + } + // Is the view's file somewhere within the plugin directory tree? // Note: we include PODS_DIR for the case of symlinks (see issue #2945). - $located = pods_validate_safe_path( $_view, [ 'plugins', 'pods', 'theme' ] ); + $located = pods_validate_safe_path( $_view, $paths_to_check ); /** * Allow filtering the validated view file path to use. diff --git a/classes/fields/code.php b/classes/fields/code.php index 1a6181a4e1..2680d79422 100644 --- a/classes/fields/code.php +++ b/classes/fields/code.php @@ -102,12 +102,11 @@ public function schema( $options = null ) { * {@inheritdoc} */ public function display( $value = null, $name = null, $options = null, $pod = null, $id = null ) { - if ( 1 === (int) pods_v( static::$type . '_allow_shortcode', $options, 0 ) ) { $value = do_shortcode( $value ); } - return $value; + return $this->maybe_sanitize_output( $value, $options ); } /** diff --git a/classes/fields/datetime.php b/classes/fields/datetime.php index 547b84d325..a9d37e69b8 100644 --- a/classes/fields/datetime.php +++ b/classes/fields/datetime.php @@ -271,10 +271,7 @@ public function is_empty( $value = null ) { * {@inheritdoc} */ public function display( $value = null, $name = null, $options = null, $pod = null, $id = null ) { - - $value = $this->format_value_display( $value, $options, false ); - - return $value; + return $this->format_value_display( $value, $options, false ); } /** diff --git a/classes/fields/number.php b/classes/fields/number.php index 524b6f2fb9..6f9c483aed 100644 --- a/classes/fields/number.php +++ b/classes/fields/number.php @@ -193,10 +193,7 @@ public function is_empty( $value = null ) { * {@inheritdoc} */ public function display( $value = null, $name = null, $options = null, $pod = null, $id = null ) { - - $value = $this->format( $value, $name, $options, $pod, $id ); - - return $value; + return $this->format( $value, $name, $options, $pod, $id ); } /** diff --git a/classes/fields/oembed.php b/classes/fields/oembed.php index 09d5baf260..de5843b195 100644 --- a/classes/fields/oembed.php +++ b/classes/fields/oembed.php @@ -143,7 +143,6 @@ public function schema( $options = null ) { * {@inheritdoc} */ public function display( $value = null, $name = null, $options = null, $pod = null, $id = null ) { - $value = $this->pre_save( $value, $id, $name, $options, null, $pod ); $width = (int) pods_v( static::$type . '_width', $options ); @@ -156,9 +155,7 @@ public function display( $value = null, $name = null, $options = null, $pod = nu $args['height'] = $height; } - $value = wp_oembed_get( $value, $args ); - - return $value; + return wp_oembed_get( $value, $args ); } /** @@ -273,7 +270,7 @@ public function strip_html( $value, $options = null ) { } // Strip HTML - $value = strip_tags( $value ); + $value = wp_strip_all_tags( $value ); // Strip shortcodes $value = strip_shortcodes( $value ); @@ -479,13 +476,18 @@ public function admin_ajax_oembed_update_preview() { if ( ! empty( $params['_nonce_pods_oembed'] ) && ! empty( $params['pods_field_oembed_value'] ) && wp_verify_nonce( $params['_nonce_pods_oembed'], 'pods_field_oembed_preview' ) ) { $name = ''; + $value = ''; $options = array(); if ( ! empty( $params['pods_field_oembed_name'] ) ) { - $name = $params['pods_field_oembed_value']; - $name = $this->strip_html( $name ); - $name = $this->strip_shortcodes( $name ); - $name = $this->trim_whitespace( $name ); + $name = $params['pods_field_oembed_name']; + } + + if ( ! empty( $params['pods_field_oembed_value'] ) ) { + $value = $params['pods_field_oembed_value']; + $value = $this->strip_html( $value ); + $value = $this->strip_shortcodes( $value ); + $value = $this->trim_whitespace( $value ); } if ( ! empty( $params['pods_field_oembed_options'] ) ) { diff --git a/classes/fields/paragraph.php b/classes/fields/paragraph.php index 63ea1d3639..7e8028c92a 100644 --- a/classes/fields/paragraph.php +++ b/classes/fields/paragraph.php @@ -145,14 +145,6 @@ public function options() { ], ]; - if ( function_exists( 'Markdown' ) ) { - $options['output_options']['boolean_group'][ static::$type . '_allow_markdown' ] = [ - 'label' => __( 'Allow Markdown Syntax', 'pods' ), - 'default' => 0, - 'type' => 'boolean', - ]; - } - return $options; } @@ -177,6 +169,23 @@ public function schema( $options = null ) { */ public function display( $value = null, $name = null, $options = null, $pod = null, $id = null ) { $value = $this->strip_html( $value, $options ); + + /** + * Allow filtering of the display value for the Paragraph field type before it's processed. + * + * NOTE: HTML has already been stripped at this point. + * + * @since 3.1.0 + * + * @param mixed|null $value Current value. + * @param string $type Field type. + * @param string|null $name Field name. + * @param array|null $options Field options. + * @param array|null $pod Pod information. + * @param int|string|null $id Current item ID. + */ + $value = apply_filters( 'pods_form_ui_field_paragraph_display_value_pre_process', $value, static::$type, $name, $options, $pod, $id ); + $value = $this->strip_shortcodes( $value, $options ); $value = $this->trim_whitespace( $value, $options ); @@ -206,11 +215,19 @@ public function display( $value = null, $name = null, $options = null, $pod = nu $value = do_shortcode( $value ); } - if ( function_exists( 'Markdown' ) && 1 === (int) pods_v( static::$type . '_allow_markdown', $options ) ) { - $value = Markdown( $value ); - } - - return $value; + /** + * Allow filtering of the display value for the Paragraph field type. + * + * @since 3.1.0 + * + * @param mixed|null $value Current value. + * @param string $type Field type. + * @param string|null $name Field name. + * @param array|null $options Field options. + * @param array|null $pod Pod information. + * @param int|string|null $id Current item ID. + */ + return apply_filters( 'pods_form_ui_field_paragraph_display_value', $value, static::$type, $name, $options, $pod, $id ); } /** diff --git a/classes/fields/pick.php b/classes/fields/pick.php index 2db0937e45..d1f1a3f7c0 100644 --- a/classes/fields/pick.php +++ b/classes/fields/pick.php @@ -2731,8 +2731,8 @@ public function get_object_data( $object_params = null ) { } else { $display_field = "`t`.`{$display_field_name}`"; } - } elseif ( isset( $table_info['object_fields'] ) && isset( $table_info['object_fields'][ $display ] ) ) { - $display_field_name = $table_info['object_fields'][ $display ]; + } elseif ( isset( $table_info['object_fields'] ) && isset( $table_info['object_fields'][ $display ]['name'] ) ) { + $display_field_name = $table_info['object_fields'][ $display ]['name']; $display_field = "`t`.`{$display_field_name}`"; }//end if diff --git a/classes/fields/wysiwyg.php b/classes/fields/wysiwyg.php index 36f8c04733..f68549d4f6 100644 --- a/classes/fields/wysiwyg.php +++ b/classes/fields/wysiwyg.php @@ -154,14 +154,6 @@ public function options() { ], ]; - if ( function_exists( 'Markdown' ) ) { - $options['output_options']['boolean_group'][ static::$type . '_allow_markdown' ] = [ - 'label' => __( 'Allow Markdown Syntax', 'pods' ), - 'default' => 0, - 'type' => 'boolean', - ]; - } - return $options; } @@ -180,6 +172,23 @@ public function schema( $options = null ) { */ public function display( $value = null, $name = null, $options = null, $pod = null, $id = null ) { $value = $this->strip_html( $value, $options ); + + /** + * Allow filtering of the display value for the WYSIWYG field type before it's processed. + * + * NOTE: HTML has already been stripped at this point. + * + * @since 3.1.0 + * + * @param mixed|null $value Current value. + * @param string $type Field type. + * @param string|null $name Field name. + * @param array|null $options Field options. + * @param array|null $pod Pod information. + * @param int|string|null $id Current item ID. + */ + $value = apply_filters( 'pods_form_ui_field_wysiwyg_display_value_pre_process', $value, static::$type, $name, $options, $pod, $id ); + $value = $this->strip_shortcodes( $value, $options ); $value = $this->trim_whitespace( $value, $options ); @@ -228,11 +237,19 @@ public function display( $value = null, $name = null, $options = null, $pod = nu $value = do_shortcode( $value ); } - if ( function_exists( 'Markdown' ) && 1 === (int) pods_v( static::$type . '_allow_markdown', $options ) ) { - $value = Markdown( $value ); - } - - return $value; + /** + * Allow filtering of the display value for the WYSIWYG field type. + * + * @since 3.1.0 + * + * @param mixed|null $value Current value. + * @param string $type Field type. + * @param string|null $name Field name. + * @param array|null $options Field options. + * @param array|null $pod Pod information. + * @param int|string|null $id Current item ID. + */ + return apply_filters( 'pods_form_ui_field_wysiwyg_display_value', $value, static::$type, $name, $options, $pod, $id ); } /** diff --git a/components/Markdown.php b/components/Markdown.php index 24eebca232..5394cd11fa 100644 --- a/components/Markdown.php +++ b/components/Markdown.php @@ -4,9 +4,9 @@ * * Name: Markdown Syntax * - * Description: Integration with Markdown (http://michelf.com/projects/php-markdown/); Adds an option to enable Markdown syntax for Paragraph text fields. + * Description: Integration with Markdown (via Parsedown https://github.com/erusev/parsedown); Adds an option to enable Markdown syntax for Paragraph Text and WYSIWYG fields. * - * Version: 1.0 + * Version: 2.0 * * Category: Field Types * @@ -14,2095 +14,63 @@ * @subpackage Markdown */ -if ( ! function_exists( 'Markdown' ) ) : - // - // Markdown - A text-to-HTML conversion tool for web writers - // - // PHP Markdown - // Copyright (c) 2004-2013 Michel Fortin - // - // - // Original Markdown - // Copyright (c) 2004-2006 John Gruber - // - // - define( 'MARKDOWN_VERSION', '1.0.2' ); - // 29 Nov 2013 - // - // Global default settings: - // - // Change to ">" for HTML output - @define( 'MARKDOWN_EMPTY_ELEMENT_SUFFIX', ' />' ); +add_filter( 'pods_form_ui_field_paragraph_display_value_pre_process', 'pods_markdown_maybe_parse_field_display_value', 10, 4 ); +add_filter( 'pods_form_ui_field_wysiwyg_display_value_pre_process', 'pods_markdown_maybe_parse_field_display_value', 10, 4 ); - // Define the width of a tab for code blocks. - @define( 'MARKDOWN_TAB_WIDTH', 4 ); - - // - // WordPress settings: - // - // Change to false to remove Markdown from posts and/or comments. - @define( 'MARKDOWN_WP_POSTS', true ); - @define( 'MARKDOWN_WP_COMMENTS', true ); - - // Standard Function Interface ### - @define( 'MARKDOWN_PARSER_CLASS', 'Markdown_Parser' ); - - /** - * @param $text - * - * @return mixed - */ - function Markdown( $text ) { - - // - // Initialize the parser and return the result of its transform method. - // - // Setup static parser variable. - static $parser; - if ( ! isset( $parser ) ) { - $parser_class = MARKDOWN_PARSER_CLASS; - $parser = new $parser_class(); - } - - // Transform text using parser. - return $parser->transform( $text ); - } - - // WordPress Plugin Interface ### - if ( isset( $wp_version ) ) { - // More details about how it works here: - // - // Post content and excerpts - // - Remove WordPress paragraph generator. - // - Run Markdown on excerpt, then remove all tags. - // - Add paragraph tag around the excerpt, but remove it for the excerpt rss. - if ( MARKDOWN_WP_POSTS ) { - remove_filter( 'the_content', 'wpautop' ); - remove_filter( 'the_content_rss', 'wpautop' ); - remove_filter( 'the_excerpt', 'wpautop' ); - add_filter( 'the_content', 'Markdown', 6 ); - add_filter( 'the_content_rss', 'Markdown', 6 ); - add_filter( 'get_the_excerpt', 'Markdown', 6 ); - add_filter( 'get_the_excerpt', 'trim', 7 ); - add_filter( 'the_excerpt', 'mdwp_add_p' ); - add_filter( 'the_excerpt_rss', 'mdwp_strip_p' ); - - remove_filter( 'content_save_pre', 'balanceTags', 50 ); - remove_filter( 'excerpt_save_pre', 'balanceTags', 50 ); - add_filter( 'the_content', 'balanceTags', 50 ); - add_filter( 'get_the_excerpt', 'balanceTags', 9 ); - } - - // Comments - // - Remove WordPress paragraph generator. - // - Remove WordPress auto-link generator. - // - Scramble important tags before passing them to the kses filter. - // - Run Markdown on excerpt then remove paragraph tags. - if ( MARKDOWN_WP_COMMENTS ) { - remove_filter( 'comment_text', 'wpautop', 30 ); - remove_filter( 'comment_text', 'make_clickable' ); - add_filter( 'pre_comment_content', 'Markdown', 6 ); - add_filter( 'pre_comment_content', 'mdwp_hide_tags', 8 ); - add_filter( 'pre_comment_content', 'mdwp_show_tags', 12 ); - add_filter( 'get_comment_text', 'Markdown', 6 ); - add_filter( 'get_comment_excerpt', 'Markdown', 6 ); - add_filter( 'get_comment_excerpt', 'mdwp_strip_p', 7 ); - - global $mdwp_hidden_tags, $mdwp_placeholders; - $mdwp_hidden_tags = explode( ' ', '

     
  • ' ); - $mdwp_placeholders = explode( ' ', str_rot13( 'pEj07ZbbBZ U1kqgh4w4p pre2zmeN6K QTi31t9pre ol0MP1jzJR ' . 'ML5IjmbRol ulANi1NsGY J7zRLJqPul liA8ctl16T K9nhooUHli' ) ); - } - - /** - * @param $text - * - * @return mixed|string - */ - function mdwp_add_p( $text ) { - - if ( ! preg_match( '{^$|^<(p|ul|ol|dl|pre|blockquote)>}i', $text ) ) { - $text = '

    ' . $text . '

    '; - $text = preg_replace( '{\n{2,}}', "

    \n\n

    ", $text ); - } - - return $text; - } - - /** - * @param $t - * - * @return mixed - */ - function mdwp_strip_p( $t ) { - - return preg_replace( '{}i', '', $t ); - } - - /** - * @param $text - * - * @return mixed - */ - function mdwp_hide_tags( $text ) { - - global $mdwp_hidden_tags, $mdwp_placeholders; - - return str_replace( $mdwp_hidden_tags, $mdwp_placeholders, $text ); - } - - /** - * @param $text - * - * @return mixed - */ - function mdwp_show_tags( $text ) { - - global $mdwp_hidden_tags, $mdwp_placeholders; - - return str_replace( $mdwp_placeholders, $mdwp_hidden_tags, $text ); - } - }//end if - - // bBlog Plugin Info ### - /** - * @return array - */ - function identify_modifier_markdown() { - - return array( - 'name' => 'markdown', - 'type' => 'modifier', - 'nicename' => 'Markdown', - 'description' => 'A text-to-HTML conversion tool for web writers', - 'authors' => 'Michel Fortin and John Gruber', - 'licence' => 'BSD-like', - 'version' => MARKDOWN_VERSION, - 'help' => 'Markdown syntax allows you to write using an easy-to-read, easy-to-write plain text format. Based on the original Perl version by John Gruber. More...', - ); - } - - // Smarty Modifier Interface ### - /** - * @param $text - * - * @return mixed - */ - function smarty_modifier_markdown( $text ) { - - return Markdown( $text ); +/** + * Maybe parse Markdown for a field display value. + * + * @since 3.1.0 + * + * @param mixed|null $value Current value. + * @param string $type Field type. + * @param string|null $name Field name. + * @param array|null $options Field options. + * + * @return mixed|null The parsed value if markdown is enabled, otherwise the value as it was originally passed. + */ +function pods_markdown_maybe_parse_field_display_value( + $value, + $type, + $name = null, + $options = null +) { + if ( ! class_exists( 'Pods__Prefixed__Parsedown' ) || 1 !== (int) pods_v( $type . '_allow_markdown', $options ) ) { + return $value; } - // Textile Compatibility Mode ### - // Rename this file to "classTextile.php" and it can replace Textile everywhere. - if ( strcasecmp( substr( __FILE__, - 16 ), 'classTextile.php' ) == 0 ) { - // Try to include PHP SmartyPants. Should be in the same directory. - @include_once 'smartypants.php'; - - // Fake Textile class. It calls Markdown instead. - - /** - * Class Textile - */ - class Textile { - - /** - * @param $text - * @param string $lite - * @param string $encode - * - * @return mixed - */ - public function TextileThis( $text, $lite = '', $encode = '' ) { - - if ( $lite == '' && $encode == '' ) { - $text = Markdown( $text ); - } - if ( function_exists( 'SmartyPants' ) ) { - $text = SmartyPants( $text ); - } - - return $text; - } - - // Fake restricted version: restrictions are not supported for now. - - /** - * @param $text - * @param string $lite - * @param string $noimage - * - * @return mixed - */ - public function TextileRestricted( $text, $lite = '', $noimage = '' ) { - - return $this->TextileThis( $text, $lite ); - } - - // Workaround to ensure compatibility with TextPattern 4.0.3. - - /** - * @param $text - * - * @return mixed - */ - public function blockLite( $text ) { - - return $text; - } - } - }//end if - - // - // Markdown Parser Class - // - - /** - * Class Markdown_Parser - */ - class Markdown_Parser { - - // Configuration Variables ### - // Change to ">" for HTML output. - public $empty_element_suffix = MARKDOWN_EMPTY_ELEMENT_SUFFIX; - public $tab_width = MARKDOWN_TAB_WIDTH; - - // Change to `true` to disallow markup or entities. - public $no_markup = false; - public $no_entities = false; - - // Predefined urls and titles for reference links and images. - public $predef_urls = array(); - public $predef_titles = array(); - - // Parser Implementation ### - // Regex to match balanced [brackets]. - // Needed to insert a maximum bracked depth while converting to PHP. - public $nested_brackets_depth = 6; - public $nested_brackets_re; - - public $nested_url_parenthesis_depth = 4; - public $nested_url_parenthesis_re; - - // Table of hash values for escaped characters: - public $escape_chars = '\`*_{}[]()>#+-.!'; - public $escape_chars_re; - - /** - * Markdown_Parser constructor. - */ - public function __construct() { - - // - // Constructor function. Initialize appropriate member variables. - // - $this->_initDetab(); - $this->prepareItalicsAndBold(); - - $this->nested_brackets_re = str_repeat( '(?>[^\[\]]+|\[', $this->nested_brackets_depth ) . str_repeat( '\])*', $this->nested_brackets_depth ); - - $this->nested_url_parenthesis_re = str_repeat( '(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth ) . str_repeat( '(?>\)))*', $this->nested_url_parenthesis_depth ); - - $this->escape_chars_re = '[' . preg_quote( $this->escape_chars ) . ']'; - - // Sort document, block, and span gamut in ascendent priority order. - asort( $this->document_gamut ); - asort( $this->block_gamut ); - asort( $this->span_gamut ); - } - - // Internal hashes used during transformation. - public $urls = array(); - public $titles = array(); - public $html_hashes = array(); - - // Status flag to avoid invalid nesting. - public $in_anchor = false; - - public function setup() { - - // - // Called before the transformation process starts to setup parser - // states. - // - // Clear global hashes. - $this->urls = $this->predef_urls; - $this->titles = $this->predef_titles; - $this->html_hashes = array(); - - $this->in_anchor = false; - } - - public function teardown() { - - // - // Called after the transformation process to clear any variable - // which may be taking up memory unnecessarly. - // - $this->urls = array(); - $this->titles = array(); - $this->html_hashes = array(); - } - - /** - * @param $text - * - * @return string - */ - public function transform( $text ) { - - // - // Main function. Performs some preprocessing on the input text - // and pass it through the document gamut. - // - $this->setup(); - - // Remove UTF-8 BOM and marker character in input, if present. - $text = preg_replace( '{^\xEF\xBB\xBF|\x1A}', '', $text ); - - // Standardize line endings: - // DOS to Unix and Mac to Unix - $text = preg_replace( '{\r\n?}', "\n", $text ); - - // Make sure $text ends with a couple of newlines: - $text .= "\n\n"; - - // Convert all tabs to spaces. - $text = $this->detab( $text ); - - // Turn block-level HTML blocks into hash entries - $text = $this->hashHTMLBlocks( $text ); - - // Strip any lines consisting only of spaces and tabs. - // This makes subsequent regexen easier to write, because we can - // match consecutive blank lines with /\n+/ instead of something - // contorted like /[ ]*\n+/ . - $text = preg_replace( '/^[ ]+$/m', '', $text ); - - // Run document gamut methods. - foreach ( $this->document_gamut as $method => $priority ) { - $text = $this->$method( $text ); - } - - $this->teardown(); - - return $text . "\n"; - } - - public $document_gamut = array( - // Strip link definitions, store in hashes. - 'stripLinkDefinitions' => 20, - - 'runBasicBlockGamut' => 30, - ); - - /** - * @param $text - * - * @return mixed - */ - public function stripLinkDefinitions( $text ) { - - // - // Strips link definitions from text, stores the URLs and titles in - // hash references. - // - $less_than_tab = $this->tab_width - 1; - - // Link defs are in the form: ^[id]: url "optional title" - $text = preg_replace_callback( '{ - ^[ ]{0,' . $less_than_tab . '}\[(.+)\][ ]?: # id = $1 - [ ]* - \n? # maybe *one* newline - [ ]* - (?: - <(.+?)> # url = $2 - | - (\S+?) # url = $3 - ) - [ ]* - \n? # maybe one newline - [ ]* - (?: - (?<=\s) # lookbehind for whitespace - ["(] - (.*?) # title = $4 - [")] - [ ]* - )? # title is optional - (?:\n+|\Z) - }xm', array( &$this, '_stripLinkDefinitions_callback' ), $text ); - - return $text; - } - - /** - * @param $matches - * - * @return string - */ - public function _stripLinkDefinitions_callback( $matches ) { - - $link_id = strtolower( $matches[1] ); - $url = $matches[2] == '' ? $matches[3] : $matches[2]; - $this->urls[ $link_id ] = $url; - $this->titles[ $link_id ] =& $matches[4]; - - return ''; - // String that will replace the block - } - - /** - * @param $text - * - * @return mixed - */ - public function hashHTMLBlocks( $text ) { - - if ( $this->no_markup ) { - return $text; - } - - $less_than_tab = $this->tab_width - 1; - - // Hashify HTML blocks: - // We only want to do this for block-level HTML tags, such as headers, - // lists, and tables. That's because we still want to wrap

    s around - // "paragraphs" that are wrapped in non-block-level tags, such as anchors, - // phrase emphasis, and spans. The list of tags we're looking for is - // hard-coded: - // - // * List "a" is made of tags which can be both inline or block-level. - // These will be treated block-level when the start tag is alone on - // its line, otherwise they're not matched here and will be taken as - // inline later. - // * List "b" is made of tags which are always block-level; - // - $block_tags_a_re = 'ins|del'; - $block_tags_b_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|' . 'script|noscript|form|fieldset|iframe|math|svg|' . 'article|section|nav|aside|hgroup|header|footer|' . 'figure'; - - // Regular expression for the content of a block tag. - $nested_tags_level = 4; - $attr = ' - (?> # optional tag attributes - \s # starts with whitespace - (?> - [^>"/]+ # text outside quotes - | - /+(?!>) # slash not followed by ">" - | - "[^"]*" # text inside double quotes (tolerate ">") - | - \'[^\']*\' # text inside single quotes (tolerate ">") - )* - )? - '; - $content = str_repeat( ' - (?> - [^<]+ # content without tag - | - <\2 # nested opening tag - ' . $attr . ' # attributes - (?> - /> - | - >', $nested_tags_level ) . // end of opening tag - '.*?' . // last level nested tag content - str_repeat( ' - # closing nested tag - ) - | - <(?!/\2\s*> # other tags with a different name - ) - )*', $nested_tags_level ); - $content2 = str_replace( '\2', '\3', $content ); - - // First, look for nested blocks, e.g.: - //

    - //
    - // tags for inner block must be indented. - //
    - //
    - // - // The outermost tags must start at the left margin for this to match, and - // the inner nested divs must be indented. - // We need to do this before the next, more liberal match, because the next - // match will start at the first `
    ` and stop at the first `
    `. - $text = preg_replace_callback( '{(?> - (?> - (?<=\n\n) # Starting after a blank line - | # or - \A\n? # the beginning of the doc - ) - ( # save in $1 - - # Match from `\n` to `\n`, handling nested tags - # in between. - - [ ]{0,' . $less_than_tab . '} - <(' . $block_tags_b_re . ')# start tag = $2 - ' . $attr . '> # attributes followed by > and \n - ' . $content . ' # content, support nesting - # the matching end tag - [ ]* # trailing spaces/tabs - (?=\n+|\Z) # followed by a newline or end of document - - | # Special version for tags of group a. - - [ ]{0,' . $less_than_tab . '} - <(' . $block_tags_a_re . ')# start tag = $3 - ' . $attr . '>[ ]*\n # attributes followed by > - ' . $content2 . ' # content, support nesting - # the matching end tag - [ ]* # trailing spaces/tabs - (?=\n+|\Z) # followed by a newline or end of document - - | # Special case just for
    . It was easier to make a special - # case than to make the other regex more complicated. - - [ ]{0,' . $less_than_tab . '} - <(hr) # start tag = $2 - ' . $attr . ' # attributes - /?> # the matching end tag - [ ]* - (?=\n{2,}|\Z) # followed by a blank line or end of document - - | # Special case for standalone HTML comments: - - [ ]{0,' . $less_than_tab . '} - (?s: - - ) - [ ]* - (?=\n{2,}|\Z) # followed by a blank line or end of document - - | # PHP and ASP-style processor instructions ( - ) - [ ]* - (?=\n{2,}|\Z) # followed by a blank line or end of document - - ) - )}Sxmi', array( &$this, '_hashHTMLBlocks_callback' ), $text ); - - return $text; - } - - /** - * @param $matches - * - * @return string - */ - public function _hashHTMLBlocks_callback( $matches ) { - - $text = $matches[1]; - $key = $this->hashBlock( $text ); - - return "\n\n$key\n\n"; - } - - /** - * @param $text - * @param string $boundary - * - * @return string - */ - public function hashPart( $text, $boundary = 'X' ) { - - // - // Called whenever a tag must be hashed when a function insert an atomic - // element in the text stream. Passing $text to through this function gives - // a unique text-token which will be reverted back when calling unhash. - // - // The $boundary argument specify what character should be used to surround - // the token. By convension, "B" is used for block elements that needs not - // to be wrapped into paragraph tags at the end, ":" is used for elements - // that are word separators and "X" is used in the general case. - // - // Swap back any tag hash found in $text so we do not have to `unhash` - // multiple times at the end. - $text = $this->unhash( $text ); - - // Then hash the block. - static $i = 0; - $key = "$boundary\x1A" . ++ $i . $boundary; - $this->html_hashes[ $key ] = $text; - - return $key; - // String that will replace the tag. - } - - /** - * @param $text - * - * @return string - */ - public function hashBlock( $text ) { - - // - // Shortcut function for hashPart with block-level boundaries. - // - return $this->hashPart( $text, 'B' ); - } - - public $block_gamut = array( - // - // These are all the transformations that form block-level - // tags like paragraphs, headers, and list items. - // - 'doHeaders' => 10, - 'doHorizontalRules' => 20, - - 'doLists' => 40, - 'doCodeBlocks' => 50, - 'doBlockQuotes' => 60, - ); - - /** - * @param $text - * - * @return string - */ - public function runBlockGamut( $text ) { - - // - // Run block gamut tranformations. - // - // We need to escape raw HTML in Markdown source before doing anything - // else. This need to be done for each block, and not only at the - // begining in the Markdown function since hashed blocks can be part of - // list items and could have been indented. Indented blocks would have - // been seen as a code block in a previous pass of hashHTMLBlocks. - $text = $this->hashHTMLBlocks( $text ); - - return $this->runBasicBlockGamut( $text ); - } - - /** - * @param $text - * - * @return string - */ - public function runBasicBlockGamut( $text ) { - - // - // Run block gamut tranformations, without hashing HTML blocks. This is - // useful when HTML blocks are known to be already hashed, like in the first - // whole-document pass. - // - foreach ( $this->block_gamut as $method => $priority ) { - $text = $this->$method( $text ); - } - - // Finally form paragraph and restore hashed blocks. - $text = $this->formParagraphs( $text ); - - return $text; - } - - /** - * @param $text - * - * @return mixed - */ - public function doHorizontalRules( $text ) { - - // Do Horizontal Rules: - return preg_replace( '{ - ^[ ]{0,3} # Leading space - ([-*_]) # $1: First marker - (?> # Repeated marker group - [ ]{0,2} # Zero, one, or two spaces. - \1 # Marker character - ){2,} # Group repeated at least twice - [ ]* # Tailing spaces - $ # End of line. - }mx', "\n" . $this->hashBlock( "empty_element_suffix" ) . "\n", $text ); - } - - public $span_gamut = array( - // - // These are all the transformations that occur *within* block-level - // tags like paragraphs, headers, and list items. - // - // Process character escapes, code spans, and inline HTML - // in one shot. - 'parseSpan' => - 30, - - // Process anchor and image tags. Images must come first, - // because ![foo][f] looks like an anchor. - 'doImages' => 10, - 'doAnchors' => 20, - - // Make links out of things like `` - // Must come after doAnchors, because you can use < and > - // delimiters in inline links like [this](). - 'doAutoLinks' => 30, - 'encodeAmpsAndAngles' => 40, - - 'doItalicsAndBold' => 50, - 'doHardBreaks' => 60, - ); - - /** - * @param $text - * - * @return mixed - */ - public function runSpanGamut( $text ) { - - // - // Run span gamut tranformations. - // - foreach ( $this->span_gamut as $method => $priority ) { - $text = $this->$method( $text ); - } - - return $text; - } - - /** - * @param $text - * - * @return mixed - */ - public function doHardBreaks( $text ) { - - // Do hard breaks: - return preg_replace_callback( '/ {2,}\n/', array( &$this, '_doHardBreaks_callback' ), $text ); - } - - /** - * @param $matches - * - * @return string - */ - public function _doHardBreaks_callback( $matches ) { - - return $this->hashPart( "empty_element_suffix\n" ); - } - - /** - * @param $text - * - * @return mixed - */ - public function doAnchors( $text ) { - - // - // Turn Markdown link shortcuts into XHTML tags. - // - if ( $this->in_anchor ) { - return $text; - } - $this->in_anchor = true; - - // - // First, handle reference-style links: [link text] [id] - // - $text = preg_replace_callback( '{ - ( # wrap whole match in $1 - \[ - (' . $this->nested_brackets_re . ') # link text = $2 - \] - - [ ]? # one optional space - (?:\n[ ]*)? # one optional newline followed by spaces - - \[ - (.*?) # id = $3 - \] - ) - }xs', array( &$this, '_doAnchors_reference_callback' ), $text ); - - // - // Next, inline-style links: [link text](url "optional title") - // - $text = preg_replace_callback( '{ - ( # wrap whole match in $1 - \[ - (' . $this->nested_brackets_re . ') # link text = $2 - \] - \( # literal paren - [ \n]* - (?: - <(.+?)> # href = $3 - | - (' . $this->nested_url_parenthesis_re . ') # href = $4 - ) - [ \n]* - ( # $5 - ([\'"]) # quote char = $6 - (.*?) # Title = $7 - \6 # matching quote - [ \n]* # ignore any spaces/tabs between closing quote and ) - )? # title is optional - \) - ) - }xs', array( &$this, '_doAnchors_inline_callback' ), $text ); - - // - // Last, handle reference-style shortcuts: [link text] - // These must come last in case you've also got [link text][1] - // or [link text](/foo) - // - $text = preg_replace_callback( '{ - ( # wrap whole match in $1 - \[ - ([^\[\]]+) # link text = $2; can\'t contain [ or ] - \] - ) - }xs', array( &$this, '_doAnchors_reference_callback' ), $text ); - - $this->in_anchor = false; - - return $text; - } - - /** - * @param $matches - * - * @return string - */ - public function _doAnchors_reference_callback( $matches ) { - - $whole_match = $matches[1]; - $link_text = $matches[2]; - $link_id =& $matches[3]; - - if ( $link_id == '' ) { - // for shortcut links like [this][] or [this]. - $link_id = $link_text; - } - - // lower-case and turn embedded newlines into spaces - $link_id = strtolower( $link_id ); - $link_id = preg_replace( '{[ ]?\n}', ' ', $link_id ); - - if ( isset( $this->urls[ $link_id ] ) ) { - $url = $this->urls[ $link_id ]; - $url = $this->encodeAttribute( $url ); - - $result = "titles[ $link_id ] ) ) { - $title = $this->titles[ $link_id ]; - $title = $this->encodeAttribute( $title ); - $result .= " title=\"$title\""; - } - - $link_text = $this->runSpanGamut( $link_text ); - $result .= ">$link_text"; - $result = $this->hashPart( $result ); - } else { - $result = $whole_match; - } - - return $result; - } - - /** - * @param $matches - * - * @return string - */ - public function _doAnchors_inline_callback( $matches ) { - - $whole_match = $matches[1]; - $link_text = $this->runSpanGamut( $matches[2] ); - $url = $matches[3] == '' ? $matches[4] : $matches[3]; - $title =& $matches[7]; - - $url = $this->encodeAttribute( $url ); - - $result = "encodeAttribute( $title ); - $result .= " title=\"$title\""; - } - - $link_text = $this->runSpanGamut( $link_text ); - $result .= ">$link_text"; - - return $this->hashPart( $result ); - } - - /** - * @param $text - * - * @return mixed - */ - public function doImages( $text ) { - - // - // Turn Markdown image shortcuts into tags. - // - // - // First, handle reference-style labeled images: ![alt text][id] - // - $text = preg_replace_callback( '{ - ( # wrap whole match in $1 - !\[ - (' . $this->nested_brackets_re . ') # alt text = $2 - \] - - [ ]? # one optional space - (?:\n[ ]*)? # one optional newline followed by spaces - - \[ - (.*?) # id = $3 - \] - - ) - }xs', array( &$this, '_doImages_reference_callback' ), $text ); + $parsedown = new Pods__Prefixed__Parsedown(); + $parsedown->setSafeMode( true ); - // - // Next, handle inline images: ![alt text](url "optional title") - // Don't forget: encode * and _ - // - $text = preg_replace_callback( '{ - ( # wrap whole match in $1 - !\[ - (' . $this->nested_brackets_re . ') # alt text = $2 - \] - \s? # One optional whitespace character - \( # literal paren - [ \n]* - (?: - <(\S*)> # src url = $3 - | - (' . $this->nested_url_parenthesis_re . ') # src url = $4 - ) - [ \n]* - ( # $5 - ([\'"]) # quote char = $6 - (.*?) # title = $7 - \6 # matching quote - [ \n]* - )? # title is optional - \) - ) - }xs', array( &$this, '_doImages_inline_callback' ), $text ); + return $parsedown->text( $value ); +} - return $text; - } - - /** - * @param $matches - * - * @return string - */ - public function _doImages_reference_callback( $matches ) { - - $whole_match = $matches[1]; - $alt_text = $matches[2]; - $link_id = strtolower( $matches[3] ); - - if ( $link_id == '' ) { - $link_id = strtolower( $alt_text ); - // for shortcut links like ![this][]. - } - - $alt_text = $this->encodeAttribute( $alt_text ); - if ( isset( $this->urls[ $link_id ] ) ) { - $url = $this->encodeAttribute( $this->urls[ $link_id ] ); - $result = "\"$alt_text\"";titles[ $link_id ] ) ) { - $title = $this->titles[ $link_id ]; - $title = $this->encodeAttribute( $title ); - $result .= " title=\"$title\""; - } - $result .= $this->empty_element_suffix; - $result = $this->hashPart( $result ); - } else { - // If there's no such link ID, leave intact: - $result = $whole_match; - } - - return $result; - } - - /** - * @param $matches - * - * @return string - */ - public function _doImages_inline_callback( $matches ) { - - $whole_match = $matches[1]; - $alt_text = $matches[2]; - $url = $matches[3] == '' ? $matches[4] : $matches[3]; - $title =& $matches[7]; - - $alt_text = $this->encodeAttribute( $alt_text ); - $url = $this->encodeAttribute( $url ); - $result = "\"$alt_text\"";encodeAttribute( $title ); - $result .= " title=\"$title\""; - // $title already quoted - } - $result .= $this->empty_element_suffix; - - return $this->hashPart( $result ); - } - - /** - * @param $text - * - * @return mixed - */ - public function doHeaders( $text ) { - - // Setext-style headers: - // Header 1 - // ======== - // - // Header 2 - // -------- - // - $text = preg_replace_callback( '{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx', array( - &$this, - '_doHeaders_callback_setext', - ), $text ); - - // atx-style headers: - // Header 1 - // Header 2 - // Header 2 with closing hashes ## - // ... - // Header 6 - // - $text = preg_replace_callback( '{ - ^(\#{1,6}) # $1 = string of #\'s - [ ]* - (.+?) # $2 = Header text - [ ]* - \#* # optional closing #\'s (not counted) - \n+ - }xm', array( &$this, '_doHeaders_callback_atx' ), $text ); - - return $text; - } - - /** - * @param $matches - * - * @return string - */ - public function _doHeaders_callback_setext( $matches ) { - - // Terrible hack to check we haven't found an empty list item. - if ( $matches[2] == '-' && preg_match( '{^-(?: |$)}', $matches[1] ) ) { - return $matches[0]; - } - - $level = $matches[2][0] == '=' ? 1 : 2; - $block = "" . $this->runSpanGamut( $matches[1] ) . ""; - - return "\n" . $this->hashBlock( $block ) . "\n\n"; - } - - /** - * @param $matches - * - * @return string - */ - public function _doHeaders_callback_atx( $matches ) { - - $level = strlen( $matches[1] ); - $block = "" . $this->runSpanGamut( $matches[2] ) . ""; - - return "\n" . $this->hashBlock( $block ) . "\n\n"; - } - - /** - * @param $text - * - * @return mixed - */ - public function doLists( $text ) { - - // - // Form HTML ordered (numbered) and unordered (bulleted) lists. - // - $less_than_tab = $this->tab_width - 1; - - // Re-usable patterns to match list item bullets and number markers: - $marker_ul_re = '[*+-]'; - $marker_ol_re = '\d+[\.]'; - $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; - - $markers_relist = array( - $marker_ul_re => $marker_ol_re, - $marker_ol_re => $marker_ul_re, - ); - - foreach ( $markers_relist as $marker_re => $other_marker_re ) { - // Re-usable pattern to match any entirel ul or ol list: - $whole_list_re = ' - ( # $1 = whole list - ( # $2 - ([ ]{0,' . $less_than_tab . '}) # $3 = number of spaces - (' . $marker_re . ') # $4 = first list item marker - [ ]+ - ) - (?s:.+?) - ( # $5 - \z - | - \n{2,} - (?=\S) - (?! # Negative lookahead for another list item marker - [ ]* - ' . $marker_re . '[ ]+ - ) - | - (?= # Lookahead for another kind of list - \n - \3 # Must have the same indentation - ' . $other_marker_re . '[ ]+ - ) - ) - ) - '; - // mx - // We use a different prefix before nested lists than top-level lists. - // See extended comment in _ProcessListItems(). - if ( $this->list_level ) { - $text = preg_replace_callback( '{ - ^ - ' . $whole_list_re . ' - }mx', array( &$this, '_doLists_callback' ), $text ); - } else { - $text = preg_replace_callback( '{ - (?:(?<=\n)\n|\A\n?) # Must eat the newline - ' . $whole_list_re . ' - }mx', array( &$this, '_doLists_callback' ), $text ); - } - }//end foreach - - return $text; - } - - /** - * @param $matches - * - * @return string - */ - public function _doLists_callback( $matches ) { - - // Re-usable patterns to match list item bullets and number markers: - $marker_ul_re = '[*+-]'; - $marker_ol_re = '\d+[\.]'; - $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; - - $list = $matches[1]; - $list_type = preg_match( "/$marker_ul_re/", $matches[4] ) ? 'ul' : 'ol'; - - $marker_any_re = ( $list_type == 'ul' ? $marker_ul_re : $marker_ol_re ); - - $list .= "\n"; - $result = $this->processListItems( $list, $marker_any_re ); - - $result = $this->hashBlock( "<$list_type>\n" . $result . "" ); - - return "\n" . $result . "\n\n"; - } - - public $list_level = 0; - - /** - * @param $list_str - * @param $marker_any_re - * - * @return mixed - */ - public function processListItems( $list_str, $marker_any_re ) { - - // - // Process the contents of a single ordered or unordered list, splitting it - // into individual list items. - // - // The $this->list_level global keeps track of when we're inside a list. - // Each time we enter a list, we increment it; when we leave a list, - // we decrement. If it's zero, we're not in a list anymore. - // - // We do this because when we're not inside a list, we want to treat - // something like this: - // - // I recommend upgrading to version - // 8. Oops, now this line is treated - // as a sub-list. - // - // As a single paragraph, despite the fact that the second line starts - // with a digit-period-space sequence. - // - // Whereas when we're inside a list (or sub-list), that line will be - // treated as the start of a sub-list. What a kludge, huh? This is - // an aspect of Markdown's syntax that's hard to parse perfectly - // without resorting to mind-reading. Perhaps the solution is to - // change the syntax rules such that sub-lists must start with a - // starting cardinal number; e.g. "1." or "a.". - $this->list_level ++; - - // trim trailing blank lines: - $list_str = preg_replace( "/\n{2,}\\z/", "\n", $list_str ); - - $list_str = preg_replace_callback( '{ - (\n)? # leading line = $1 - (^[ ]*) # leading whitespace = $2 - (' . $marker_any_re . ' # list marker and space = $3 - (?:[ ]+|(?=\n)) # space only required if item is not empty - ) - ((?s:.*?)) # list item text = $4 - (?:(\n+(?=\n))|\n) # tailing blank line = $5 - (?= \n* (\z | \2 (' . $marker_any_re . ') (?:[ ]+|(?=\n)))) - }xm', array( &$this, '_processListItems_callback' ), $list_str ); - - $this->list_level --; - - return $list_str; - } - - /** - * @param $matches - * - * @return string - */ - public function _processListItems_callback( $matches ) { - - $item = $matches[4]; - $leading_line =& $matches[1]; - $leading_space =& $matches[2]; - $marker_space = $matches[3]; - $tailing_blank_line =& $matches[5]; - - if ( $leading_line || $tailing_blank_line || preg_match( '/\n{2,}/', $item ) ) { - // Replace marker with the appropriate whitespace indentation - $item = $leading_space . str_repeat( ' ', strlen( $marker_space ) ) . $item; - $item = $this->runBlockGamut( $this->outdent( $item ) . "\n" ); - } else { - // Recursion for sub-lists: - $item = $this->doLists( $this->outdent( $item ) ); - $item = preg_replace( '/\n+$/', '', $item ); - $item = $this->runSpanGamut( $item ); - } - - return '
  • ' . $item . "
  • \n"; - } - - /** - * @param $text - * - * @return mixed - */ - public function doCodeBlocks( $text ) { - - // - // Process Markdown `
    ` blocks.
    -			//
    -			$text = preg_replace_callback( '{
    -				(?:\n\n|\A\n?)
    -				(	            # $1 = the code block -- one or more lines, starting with a space/tab
    -				  (?>
    -					[ ]{' . $this->tab_width . '}  # Lines must start with a tab or a tab-width of spaces
    -					.*\n+
    -				  )+
    -				)
    -				((?=^[ ]{0,' . $this->tab_width . '}\S)|\Z)	# Lookahead for non-space at line-start, or end of doc
    -			}xm', array( &$this, '_doCodeBlocks_callback' ), $text );
    -
    -			return $text;
    -		}
    -
    -		/**
    -		 * @param $matches
    -		 *
    -		 * @return string
    -		 */
    -		public function _doCodeBlocks_callback( $matches ) {
    -
    -			$codeblock = $matches[1];
    -
    -			$codeblock = $this->outdent( $codeblock );
    -			$codeblock = htmlspecialchars( $codeblock, ENT_NOQUOTES );
    -
    -			// trim leading newlines and trailing newlines
    -			$codeblock = preg_replace( '/\A\n+|\n+\z/', '', $codeblock );
    -
    -			$codeblock = "
    $codeblock\n
    "; - - return "\n\n" . $this->hashBlock( $codeblock ) . "\n\n"; - } - - /** - * @param $code - * - * @return string - */ - public function makeCodeSpan( $code ) { - - // - // Create a code span markup for $code. Called from handleSpanToken. - // - $code = htmlspecialchars( trim( $code ), ENT_NOQUOTES ); - - return $this->hashPart( "$code" ); - } - - public $em_relist = array( - '' => '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(?em_relist as $em => $em_re ) { - foreach ( $this->strong_relist as $strong => $strong_re ) { - // Construct list of allowed token expressions. - $token_relist = array(); - if ( isset( $this->em_strong_relist["$em$strong"] ) ) { - $token_relist[] = $this->em_strong_relist["$em$strong"]; - } - $token_relist[] = $em_re; - $token_relist[] = $strong_re; - - // Construct master expression from list. - $token_re = '{(' . implode( '|', $token_relist ) . ')}'; - $this->em_strong_prepared_relist["$em$strong"] = $token_re; - } - } - } - - /** - * @param $text - * - * @return string - */ - public function doItalicsAndBold( $text ) { - - $token_stack = array( '' ); - $text_stack = array( '' ); - $em = ''; - $strong = ''; - $tree_char_em = false; - - while ( 1 ) { - // - // Get prepared regular expression for seraching emphasis tokens - // in current context. - // - $token_re = $this->em_strong_prepared_relist["$em$strong"]; - - // - // Each loop iteration search for the next emphasis token. - // Each token is then passed to handleSpanToken. - // - $parts = preg_split( $token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE ); - $text_stack[0] .= $parts[0]; - $token =& $parts[1]; - $text =& $parts[2]; - - if ( empty( $token ) ) { - // Reached end of text span: empty stack without emitting. - // any more emphasis. - while ( $token_stack[0] ) { - $text_stack[1] .= array_shift( $token_stack ); - $text_stack[0] .= array_shift( $text_stack ); - } - break; - } - - $token_len = strlen( $token ); - if ( $tree_char_em ) { - // Reached closing marker while inside a three-char emphasis. - if ( $token_len == 3 ) { - // Three-char closing marker, close em and strong. - array_shift( $token_stack ); - $span = array_shift( $text_stack ); - $span = $this->runSpanGamut( $span ); - $span = "$span"; - $text_stack[0] .= $this->hashPart( $span ); - $em = ''; - $strong = ''; - } else { - // Other closing marker: close one em or strong and - // change current token state to match the other - $token_stack[0] = str_repeat( $token[0], 3 - $token_len ); - $tag = $token_len == 2 ? 'strong' : 'em'; - $span = $text_stack[0]; - $span = $this->runSpanGamut( $span ); - $span = "<$tag>$span"; - $text_stack[0] = $this->hashPart( $span ); - $$tag = ''; - // $$tag stands for $em or $strong - }//end if - $tree_char_em = false; - } elseif ( $token_len == 3 ) { - if ( $em ) { - // Reached closing marker for both em and strong. - // Closing strong marker: - for ( $i = 0; $i < 2; ++ $i ) { - $shifted_token = array_shift( $token_stack ); - $tag = strlen( $shifted_token ) == 2 ? 'strong' : 'em'; - $span = array_shift( $text_stack ); - $span = $this->runSpanGamut( $span ); - $span = "<$tag>$span"; - $text_stack[0] .= $this->hashPart( $span ); - $$tag = ''; - // $$tag stands for $em or $strong - } - } else { - // Reached opening three-char emphasis marker. Push on token - // stack; will be handled by the special condition above. - $em = $token[0]; - $strong = "$em$em"; - array_unshift( $token_stack, $token ); - array_unshift( $text_stack, '' ); - $tree_char_em = true; - }//end if - } elseif ( $token_len == 2 ) { - if ( $strong ) { - // Unwind any dangling emphasis marker: - if ( strlen( $token_stack[0] ) == 1 ) { - $text_stack[1] .= array_shift( $token_stack ); - $text_stack[0] .= array_shift( $text_stack ); - } - // Closing strong marker: - array_shift( $token_stack ); - $span = array_shift( $text_stack ); - $span = $this->runSpanGamut( $span ); - $span = "$span"; - $text_stack[0] .= $this->hashPart( $span ); - $strong = ''; - } else { - array_unshift( $token_stack, $token ); - array_unshift( $text_stack, '' ); - $strong = $token; - } - } else { - // Here $token_len == 1 - if ( $em ) { - if ( strlen( $token_stack[0] ) == 1 ) { - // Closing emphasis marker: - array_shift( $token_stack ); - $span = array_shift( $text_stack ); - $span = $this->runSpanGamut( $span ); - $span = "$span"; - $text_stack[0] .= $this->hashPart( $span ); - $em = ''; - } else { - $text_stack[0] .= $token; - } - } else { - array_unshift( $token_stack, $token ); - array_unshift( $text_stack, '' ); - $em = $token; - } - }//end if - }//end while - - return $text_stack[0]; - } - - /** - * @param $text - * - * @return mixed - */ - public function doBlockQuotes( $text ) { - - $text = preg_replace_callback( '/ - ( # Wrap whole match in $1 - (?> - ^[ ]*>[ ]? # ">" at the start of a line - .+\n # rest of the first line - (.+\n)* # subsequent consecutive lines - \n* # blanks - )+ - ) - /xm', array( &$this, '_doBlockQuotes_callback' ), $text ); - - return $text; - } - - /** - * @param $matches - * - * @return string - */ - public function _doBlockQuotes_callback( $matches ) { - - $bq = $matches[1]; - // trim one level of quoting - trim whitespace-only lines - $bq = preg_replace( '/^[ ]*>[ ]?|^[ ]+$/m', '', $bq ); - $bq = $this->runBlockGamut( $bq ); - // recurse - $bq = preg_replace( '/^/m', ' ', $bq ); - // These leading spaces cause problem with
     content,
    -			// so we need to fix that:
    -			$bq = preg_replace_callback( '{(\s*
    .+?
    )}sx', array( &$this, '_doBlockQuotes_callback2' ), $bq ); - - return "\n" . $this->hashBlock( "
    \n$bq\n
    " ) . "\n\n"; - } - - /** - * @param $matches - * - * @return mixed - */ - public function _doBlockQuotes_callback2( $matches ) { - - $pre = $matches[1]; - $pre = preg_replace( '/^ /m', '', $pre ); - - return $pre; - } - - /** - * @param $text - * - * @return string - */ - public function formParagraphs( $text ) { - - // - // Params: - // $text - string to process with html

    tags - // - // Strip leading and trailing lines: - $text = preg_replace( '/\A\n+|\n+\z/', '', $text ); - - $grafs = preg_split( '/\n{2,}/', $text, - 1, PREG_SPLIT_NO_EMPTY ); - - // - // Wrap

    tags and unhashify HTML blocks - // - foreach ( $grafs as $key => $value ) { - if ( ! preg_match( '/^B\x1A[0-9]+B$/', $value ) ) { - // Is a paragraph. - $value = $this->runSpanGamut( $value ); - $value = preg_replace( '/^([ ]*)/', '

    ', $value ); - $value .= '

    '; - $grafs[ $key ] = $this->unhash( $value ); - } else { - // Is a block. - // Modify elements of @grafs in-place... - $graf = $value; - $block = $this->html_hashes[ $graf ]; - $graf = $block; - // if (preg_match('{ - // \A - // ( # $1 =
    tag - //
    ]* - // \b - // markdown\s*=\s* ([\'"]) # $2 = attr quote char - // 1 - // \2 - // [^>]* - // > - // ) - // ( # $3 = contents - // .* - // ) - // (
    ) # $4 = closing tag - // \z - // }xs', $block, $matches)) - // { - // list(, $div_open, , $div_content, $div_close) = $matches; - // - // # We can't call Markdown(), because that resets the hash; - // # that initialization code should be pulled into its own sub, though. - // $div_content = $this->hashHTMLBlocks($div_content); - // - // # Run document gamut methods on the content. - // foreach ($this->document_gamut as $method => $priority) { - // $div_content = $this->$method($div_content); - // } - // - // $div_open = preg_replace( - // '{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open); - // - // $graf = $div_open . "\n" . $div_content . "\n" . $div_close; - // } - $grafs[ $key ] = $graf; - }//end if - }//end foreach - - return implode( "\n\n", $grafs ); - } - - /** - * @param $text - * - * @return mixed - */ - public function encodeAttribute( $text ) { - - // - // Encode text for a double-quoted HTML attribute. This function - // is *not* suitable for attributes enclosed in single quotes. - // - $text = $this->encodeAmpsAndAngles( $text ); - $text = str_replace( '"', '"', $text ); - - return $text; - } - - /** - * @param $text - * - * @return mixed - */ - public function encodeAmpsAndAngles( $text ) { - - // - // Smart processing for ampersands and angle brackets that need to - // be encoded. Valid character entities are left alone unless the - // no-entities mode is set. - // - if ( $this->no_entities ) { - $text = str_replace( '&', '&', $text ); - } else { - // Ampersand-encoding based entirely on Nat Irons's Amputator - // MT plugin: - $text = preg_replace( '/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/', '&', $text ); - } - // Encode remaining <'s - $text = str_replace( '<', '<', $text ); - - return $text; - } - - /** - * @param $text - * - * @return mixed - */ - public function doAutoLinks( $text ) { - - $text = preg_replace_callback( '{<((https?|ftp|dict):[^\'">\s]+)>}i', array( - &$this, - '_doAutoLinks_url_callback', - ), $text ); - - // Email addresses: - $text = preg_replace_callback( '{ - < - (?:mailto:)? - ( - (?: - [-!#$%&\'*+/=?^_`.{|}~\w\x80-\xFF]+ - | - ".*?" - ) - \@ - (?: - [-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+ - | - \[[\d.a-fA-F:]+\] # IPv4 & IPv6 - ) - ) - > - }xi', array( &$this, '_doAutoLinks_email_callback' ), $text ); - $text = preg_replace_callback( '{<(tel:([^\'">\s]+))>}i', array( - &$this, - '_doAutoLinks_tel_callback', - ), $text ); - - return $text; - } - - /** - * @param $matches - * - * @return string - */ - public function _doAutoLinks_tel_callback( $matches ) { - - $url = $this->encodeAttribute( $matches[1] ); - $tel = $this->encodeAttribute( $matches[2] ); - $link = "$tel"; - - return $this->hashPart( $link ); - } - - /** - * @param $matches - * - * @return string - */ - public function _doAutoLinks_url_callback( $matches ) { - - $url = $this->encodeAttribute( $matches[1] ); - $link = "$url"; - - return $this->hashPart( $link ); - } - - /** - * @param $matches - * - * @return string - */ - public function _doAutoLinks_email_callback( $matches ) { - - $address = $matches[1]; - $link = $this->encodeEmailAddress( $address ); - - return $this->hashPart( $link ); - } - - /** - * @param $addr - * - * @return string - */ - public function encodeEmailAddress( $addr ) { - - // - // Input: an email address, e.g. "foo@example.com" - // - // Output: the email address as a mailto link, with each character - // of the address encoded as either a decimal or hex entity, in - // the hopes of foiling most address harvesting spam bots. E.g.: - // - //

    foo@exampl - // e.com

    - // - // Based by a filter by Matthew Wickline, posted to BBEdit-Talk. - // With some optimizations by Milian Wolff. - // - $addr = 'mailto:' . $addr; - $chars = preg_split( '/(? $char ) { - $ord = ord( $char ); - // Ignore non-ascii chars. - if ( $ord < 128 ) { - $r = ( $seed * ( 1 + $key ) ) % 100; - // Pseudo-random function. - // roughly 10% raw, 45% hex, 45% dec - // '@' *must* be encoded. I insist. - if ( $r > 90 && $char != '@' ) { /* do nothing */ - } elseif ( $r < 45 ) { - $chars[ $key ] = '&#x' . dechex( $ord ) . ';'; - } else { - $chars[ $key ] = '&#' . $ord . ';'; - } - } - } - - $addr = implode( '', $chars ); - $text = implode( '', array_slice( $chars, 7 ) ); - // text without `mailto:` - $addr = "$text"; - - return $addr; - } - - /** - * @param $str - * - * @return string - */ - public function parseSpan( $str ) { - - // - // Take the string $str and parse it into tokens, hashing embeded HTML, - // escaped characters and handling code spans. - // - $output = ''; - - $span_re = '{ - ( - \\\\' . $this->escape_chars_re . ' - | - (?no_markup ? '' : ' - | - # comment - | - <\?.*?\?> | <%.*?%> # processing instruction - | - <[!$]?[-a-zA-Z0-9:_]+ # regular tags - (?> - \s - (?>[^"\'>]+|"[^"]*"|\'[^\']*\')* - )? - > - | - <[-a-zA-Z0-9:_]+\s*/> # xml-style empty tag - | - # closing tag - ' ) . ' - ) - }xs'; - - while ( 1 ) { - // - // Each loop iteration seach for either the next tag, the next - // openning code span marker, or the next escaped character. - // Each token is then passed to handleSpanToken. - // - $parts = preg_split( $span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE ); - - // Create token from text preceding tag. - if ( $parts[0] != '' ) { - $output .= $parts[0]; - } - - // Check if we reach the end. - if ( isset( $parts[1] ) ) { - $output .= $this->handleSpanToken( $parts[1], $parts[2] ); - $str = $parts[2]; - } else { - break; - } - }//end while - - return $output; - } - - /** - * @param $token - * @param $str - * - * @return string - */ - public function handleSpanToken( $token, &$str ) { - - // - // Handle $token provided by parseSpan by determining its nature and - // returning the corresponding value that should replace it. - // - switch ( $token[0] ) { - case '\\': - return $this->hashPart( '&#' . ord( $token[1] ) . ';' ); - case '`': - // Search for end marker in remaining text. - if ( preg_match( '/^(.*?[^`])' . preg_quote( $token ) . '(?!`)(.*)$/sm', $str, $matches ) ) { - $str = $matches[2]; - $codespan = $this->makeCodeSpan( $matches[1] ); - - return $this->hashPart( $codespan ); - } - - return $token; - // return as text since no ending marker found. - default: - return $this->hashPart( $token ); - } - } - - /** - * @param $text - * - * @return mixed - */ - public function outdent( $text ) { - - // - // Remove one level of line-leading tabs or spaces - // - return preg_replace( '/^(\t|[ ]{1,' . $this->tab_width . '})/m', '', $text ); - } - - - // String length function for detab. `_initDetab` will create a function to - // hanlde UTF-8 if the default function does not exist. - public $utf8_strlen = 'mb_strlen'; - - /** - * @param $text - * - * @return mixed - */ - public function detab( $text ) { - - // - // Replace tabs with the appropriate amount of space. - // - // For each line we separate the line in blocks delemited by - // tab characters. Then we reconstruct every line by adding the - // appropriate number of space between each blocks. - $text = preg_replace_callback( '/^.*\t.*$/m', array( &$this, '_detab_callback' ), $text ); - - return $text; - } - - /** - * @param $matches - * - * @return string - */ - public function _detab_callback( $matches ) { - - $line = $matches[0]; - $strlen = $this->utf8_strlen; - // strlen function for UTF-8. - // Split in blocks. - $blocks = explode( "\t", $line ); - // Add each blocks to the line. - $line = $blocks[0]; - unset( $blocks[0] ); - // Do not add first block twice. - foreach ( $blocks as $block ) { - // Calculate amount of space, insert spaces, insert block. - $amount = $this->tab_width - $strlen( $line, 'UTF-8' ) % $this->tab_width; - $line .= str_repeat( ' ', $amount ) . $block; - } - - return $line; - } - - public function _initDetab() { - - // - // Check for the availability of the function in the `utf8_strlen` property - // (initially `mb_strlen`). If the function is not available, create a - // function that will loosely count the number of UTF-8 characters with a - // regular expression. - // - if ( function_exists( $this->utf8_strlen ) ) { - return; - } - $this->utf8_strlen = create_function( '$text', 'return preg_match_all( - "/[\\\\x00-\\\\xBF]|[\\\\xC0-\\\\xFF][\\\\x80-\\\\xBF]*/", - $text, $m);' ); - } - - /** - * @param $text - * - * @return mixed - */ - public function unhash( $text ) { - - // - // Swap back in all the tags hashed by _HashHTMLBlocks. - // - return preg_replace_callback( '/(.)\x1A[0-9]+\1/', array( &$this, '_unhash_callback' ), $text ); - } - - /** - * @param $matches - * - * @return mixed - */ - public function _unhash_callback( $matches ) { - - return $this->html_hashes[ $matches[0] ]; - } +add_filter( 'pods_admin_setup_edit_paragraph_additional_field_options', 'pods_markdown_maybe_add_field_display_option', 10, 2 ); +add_filter( 'pods_admin_setup_edit_wysiwyg_additional_field_options', 'pods_markdown_maybe_add_field_display_option', 10, 2 ); +/** + * Maybe add field display option for Markdown. + * + * @since 3.1.0 + * + * @param array $type_options The additional field type options. + * @param string $type Field type. + * + * @return array The additional field type options. + */ +function pods_markdown_maybe_add_field_display_option( + $type_options, + $type +) { + if ( ! class_exists( 'Pods__Prefixed__Parsedown' ) || ! isset( $type_options['output_options']['boolean_group'] ) ) { + return $type_options; } - /* - PHP Markdown - ============ - - Description - ----------- - - This is a PHP translation of the original Markdown formatter written in - Perl by John Gruber. - - Markdown is a text-to-HTML filter; it translates an easy-to-read / - easy-to-write structured text format into HTML. Markdown's text format - is mostly similar to that of plain text email, and supports features such - as headers, *emphasis*, code blocks, blockquotes, and links. - - Markdown's syntax is designed not as a generic markup language, but - specifically to serve as a front-end to (X)HTML. You can use span-level - HTML tags anywhere in a Markdown document, and you can use block level - HTML tags (like
    and
    - + >
    as well). - - For more information about Markdown's syntax, see: - - - - - Bugs - ---- - - To file bug reports please send email to: - - - - Please include with your report: (1) the example input; (2) the output you - expected; (3) the output Markdown actually produced. - - - Version History - --------------- - - See the readme file for detailed release notes for this version. - - - Copyright and License - --------------------- - - PHP Markdown - Copyright (c) 2004-2013 Michel Fortin - - All rights reserved. - - Based on Markdown - Copyright (c) 2003-2006 John Gruber - - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are - met: - - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - * Neither the name "Markdown" nor the names of its contributors may - be used to endorse or promote products derived from this software - without specific prior written permission. - - This software is provided by the copyright holders and contributors "as - is" and any express or implied warranties, including, but not limited - to, the implied warranties of merchantability and fitness for a - particular purpose are disclaimed. In no event shall the copyright owner - or contributors be liable for any direct, indirect, incidental, special, - exemplary, or consequential damages (including, but not limited to, - procurement of substitute goods or services; loss of use, data, or - profits; or business interruption) however caused and on any theory of - liability, whether in contract, strict liability, or tort (including - negligence or otherwise) arising in any way out of the use of this - software, even if advised of the possibility of such damage. + $type_options['output_options']['boolean_group'][ $type . '_allow_markdown' ] = [ + 'label' => __( 'Allow Markdown Syntax', 'pods' ), + 'default' => 0, + 'type' => 'boolean', + ]; - */ -endif; + return $type_options; +} diff --git a/components/Templates/Templates.php b/components/Templates/Templates.php index e1e7f7386a..0db5e5d524 100644 --- a/components/Templates/Templates.php +++ b/components/Templates/Templates.php @@ -523,11 +523,12 @@ public function save_meta( $_null, $post_ID = null, $meta_key = null, $meta_valu * @param string $code Custom template code to use instead * @param object $obj The Pods object * @param bool $deprecated Whether to use deprecated functionality based on old function usage + * @param bool $check_access Whether to check access for Posts that are Password-protected. * * @return mixed|string|void * @since 2.0.0 */ - public static function template( $template_name, $code = null, $obj = null, $deprecated = false ) { + public static function template( $template_name, $code = null, $obj = null, $deprecated = false, $check_access = false ) { if ( ! empty( $obj ) ) { self::$obj =& $obj; @@ -603,16 +604,41 @@ public static function template( $template_name, $code = null, $obj = null, $dep $code = apply_filters( 'pods_templates_pre_template', $code, $template, $obj ); $code = apply_filters( "pods_templates_pre_template_{$slug}", $code, $template, $obj ); + $info = $check_access ? pods_info_from_args( [ 'pods' => $obj ] ) : []; + ob_start(); if ( ! empty( $code ) ) { // Only detail templates need $this->id if ( empty( $obj->id ) ) { while ( $obj->fetch() ) { + $info['item_id'] = $obj->id(); + + // Ensure the post is not password protected. + if ( + $check_access + && ( + pods_access_bypass_post_with_password( $info ) + || pods_access_bypass_private_post( $info ) + ) + ) { + continue; + } + echo self::do_template( $code, $obj ); } } else { - echo self::do_template( $code, $obj ); + $info['item_id'] = $obj->id(); + + if ( + ! $check_access + || ( + ! pods_access_bypass_post_with_password( $info ) + && ! pods_access_bypass_private_post( $info ) + ) + ) { + echo self::do_template( $code, $obj ); + } } } elseif ( $template_name == trim( preg_replace( '/[^a-zA-Z0-9_\-\/]/', '', $template_name ), ' /-' ) ) { $default_templates = array( @@ -625,10 +651,33 @@ public static function template( $template_name, $code = null, $obj = null, $dep if ( empty( $obj->id ) ) { while ( $obj->fetch() ) { + $info['item_id'] = $obj->id(); + + // Ensure the post is not password protected. + if ( + $check_access + && ( + pods_access_bypass_post_with_password( $info ) + || pods_access_bypass_private_post( $info ) + ) + ) { + continue; + } + pods_template_part( $default_templates, compact( array_keys( get_defined_vars() ) ) ); } } else { - pods_template_part( $default_templates, compact( array_keys( get_defined_vars() ) ) ); + $info['item_id'] = $obj->id(); + + if ( + ! $check_access + || ( + ! pods_access_bypass_post_with_password( $info ) + && ! pods_access_bypass_private_post( $info ) + ) + ) { + pods_template_part( $default_templates, compact( array_keys( get_defined_vars() ) ) ); + } } }//end if diff --git a/components/Templates/includes/functions-view_template.php b/components/Templates/includes/functions-view_template.php index 33f43d04eb..fdf8f8dc5b 100644 --- a/components/Templates/includes/functions-view_template.php +++ b/components/Templates/includes/functions-view_template.php @@ -503,12 +503,14 @@ function frontier_do_subtemplate( $atts, $content ) { $target_id = $entry['term_id']; } - $out .= pods_shortcode( + $out .= pods_shortcode_run_safely( array( 'name' => $field['pick_val'], 'slug' => $target_id, 'index' => $key, - ), $template + ), + $template, + false ); }//end foreach diff --git a/composer.json b/composer.json index 7a32d4c487..bba36a84a9 100644 --- a/composer.json +++ b/composer.json @@ -58,12 +58,13 @@ ], "require": { "composer/installers": "2.2.*", - "lucatume/di52": "^3.3.0", + "erusev/parsedown": "^1.7.4", + "lucatume/di52": "^3.3.5", "mustangostang/spyc": "^0.6.3", "php": ">=7.2" }, "require-dev": { - "automattic/vipwpcs": "^2.0", + "automattic/vipwpcs": "^3.0.0", "bvanhoekelen/performance": "^2.5.1", "codeception/codeception": "^4.0", "codeception/module-asserts": "^1.0", @@ -84,9 +85,9 @@ "phpcompatibility/phpcompatibility-wp": "*", "phpunit/phpunit": "^6.5", "spatie/phpunit-snapshot-assertions": "^1.4.2", - "the-events-calendar/coding-standards": "dev-master", + "the-events-calendar/coding-standards": "dev-main", "wp-cli/wp-cli": "2.*", - "wp-coding-standards/wpcs": "^2.1" + "wp-coding-standards/wpcs": "^3.0.1" }, "minimum-stability": "stable", "prefer-stable": true, @@ -95,7 +96,7 @@ }, "scripts": { "strauss": [ - "test -f ./bin/strauss.phar || curl -o bin/strauss.phar -L -C - https://github.com/BrianHenryIE/strauss/releases/download/0.14.0/strauss.phar", + "test -f ./bin/strauss.phar || curl -o bin/strauss.phar -L -C - https://github.com/BrianHenryIE/strauss/releases/latest/download/strauss.phar", "@php bin/strauss.phar" ], "post-install-cmd": [ @@ -114,6 +115,7 @@ "constant_prefix": "PODS_PREFIXED_", "function_prefix": "pods_prefixed_", "packages": [ + "erusev/parsedown", "lucatume/di52", "mustangostang/spyc" ], diff --git a/includes/access.php b/includes/access.php new file mode 100644 index 0000000000..dccc2eafb3 --- /dev/null +++ b/includes/access.php @@ -0,0 +1,2344 @@ + null, + 'object_name' => null, + 'item_id' => null, + 'pods' => null, + 'pod' => null, + ]; + + $build_pods = false; + $build_pod = false; + + if ( isset( $args['build_pods'] ) ) { + $build_pods = $args['build_pods']; + + unset( $args['build_pods'] ); + } + + if ( isset( $args['build_pod'] ) ) { + $build_pod = $args['build_pod']; + + unset( $args['build_pod'] ); + } + + // Merge in the args with the defaults. + $info = array_merge( $info, $args ); + + $object_type_set = null !== $info['object_type']; + $object_name_set = null !== $info['object_name']; + + // Maybe auto-set the object name from the type if we can. + if ( + $object_type_set + && ! $object_name_set + && in_array( $info['object_type'], [ 'comment', 'media', 'user' ], true ) + ) { + $info['object_name'] = $info['object_type']; + + $object_name_set = true; + } + + // Normalize the Pods info to null if it's not valid. + if ( + $info['pods'] instanceof Pods + && ! $info['pods']->is_valid() + ) { + $info['pods'] = null; + } + + // Maybe build the Pods object from the info. + if ( + $build_pods + && $object_name_set + && ! $info['pods'] instanceof Pods + ) { + $pods = pods_get_instance( $info['object_name'], $info['item_id'], true ); + + if ( + $pods instanceof Pods + && $pods->is_valid() + && ( + empty( $info['object_type'] ) + || $info['object_type'] === $pods->pod_data->get_type() + ) + ) { + $info['pods'] = $pods; + + if ( ! $info['pod'] instanceof Pod ) { + $info['pod'] = clone $pods->pod_data; + } + } + } elseif ( + $info['pods'] instanceof Pods + && $info['pods']->is_valid() + && ! $info['pod'] instanceof Pod + ) { + $info['pod'] = clone $info['pods']->pod_data; + } + + // Maybe build the Pod object from the info. + if ( + $build_pod + && $object_name_set + && ! $info['pod'] instanceof Pod + ) { + try { + $pod = pods_api()->load_pod( [ + 'name' => $info['object_name'], + ] ); + } catch ( Exception $e ) { + $pod = null; + } + + if ( + $pod instanceof Pod + && ( + empty( $info['object_type'] ) + || $info['object_type'] === $pod->get_type() + ) + ) { + $info['pod'] = $pod; + } + } + + if ( $info['pod'] instanceof Pod ) { + $info['object_type'] = $info['pod']->get_type(); + $info['object_name'] = $info['pod']->get_name(); + } + + return $info; +} + +/** + * Determine whether the current user has access to an object. + * + * @since 3.1.0 + * + * @param array $args { + * The arguments to use. + * + * @type string|null $object_type The object type. + * @type string|null $object_name The object name. + * @type int|string|null $item_id The item ID. + * @type Pods|null $pods The Pods object. + * @type Pod|null $pod The Pod object. + * @type bool $build_pods Whether to try to build a Pods object from the object type/name/ID (false by default). + * @type bool $build_pod Whether to try to build a Pod object from the object type/name (false by default). + * } + * @param int|null $user_id The user ID to check against, set to 0 or null for anonymous access check. + * @param string $access_type The type of access to check for (read, add, edit, delete). + * @param string|null $context The unique slug that can be referenced by hooks for context. + * + * @return bool Whether the current user has access to an object. + */ +function pods_user_can_access_object( array $args, ?int $user_id, string $access_type = 'edit', ?string $context = null ): bool { + $info = pods_info_from_args( $args ); + + if ( null === $user_id ) { + $user_id = 0; + } + + // Check if the user exists. + $user = get_userdata( $user_id ); + + if ( ! $user || is_wp_error( $user ) ) { + // If the user does not exist and it was not anonymous, do not allow access to an invalid user. + if ( 0 < $user_id ) { + return false; + } + + // If the user was 0 to begin with (anonymous) then set up a user object to work with. + $user = new WP_User(); + } + + // Determine if this is a user in WP that has full access. + if ( $user_id && pods_is_user_admin( $user_id ) ) { + return true; + } + + if ( 'pod' === $info['object_type'] || 'table' === $info['object_type'] ) { + // If no object name is provided, we cannot check access. + if ( empty( $info['object_name'] ) ) { + return false; + } + + // Determine if this user has full content access. + if ( $user->has_cap('pods_content' ) ) { + return true; + } + } + + $capabilities = pods_access_map_capabilities( $info, $user_id ); + + // Unsupported capabilities returned. + if ( null === $capabilities ) { + return false; + } + + /** + * Allow filtering the list of capabilities used for checking access against an object. + * + * @since 3.1.0 + * + * @param array $capabilities The list of capabilities used for checking access against an object. + * @param int $user_id The user ID to check against. + * @param array $info { + * The normalized Pod information referenced. + * + * @type string|null $object_type The object type (if set). + * @type string|null $object_name The object name (if set). + * @type int|string|null $item_id The item ID (if set). + * @type Pods|null $pods The Pods object (if built or provided). + * @type Pod|null $pod The Pod object (if built or provided). + * } + * @param string $access_type The type of access to check for (read, add, edit, delete). + * @param string|null $context The unique slug that can be referenced by hooks for context. + */ + $capabilities = (array) apply_filters( + 'pods_user_can_access_object_get_capabilities', + $capabilities, + $user_id, + $info, + $access_type, + $context + ); + + // No capability mapped, do not allow access. + if ( ! array_key_exists( $access_type, $capabilities ) ) { + return false; + } + + /** + * Allow filtering whether a user has access to an object before the normal capability check runs. + * + * @since 3.1.0 + * + * @param null|bool $can_access Whether a user has access to an object (return null to run normal check). + * @param int $user_id The user ID to check against. + * @param array $info { + * The normalized Pod information referenced. + * + * @type string|null $object_type The object type (if set). + * @type string|null $object_name The object name (if set). + * @type int|string|null $item_id The item ID (if set). + * @type Pods|null $pods The Pods object (if built or provided). + * @type Pod|null $pod The Pod object (if built or provided). + * } + * @param string $access_type The type of access to check for (read, add, edit, delete). + * @param string|null $context The unique slug that can be referenced by hooks for context. + * @param array $capabilities The list of capabilities used for checking access against an object. + */ + $can_access = apply_filters( + 'pods_user_can_access_object_pre_check', + null, + $user_id, + $info, + $access_type, + $context, + $capabilities + ); + + // Check for access override and return that instead. + if ( null !== $can_access ) { + return $can_access; + } + + // If we are allowing all access, null will be set for the capability. + if ( null === $capabilities[ $access_type ] ) { + $can_access = true; + } else { + // Support multiple capability checks ("OR" logic). + $capabilities[ $access_type ] = (array) $capabilities[ $access_type ]; + + $can_access = false; + + foreach ( $capabilities[ $access_type ] as $capability ) { + if ( $info['item_id'] ) { + $can_access = $user->has_cap( $capability, $info['item_id'] ); + } else { + $can_access = $user->has_cap( $capability ); + } + + if ( $can_access ) { + break; + } + } + } + + $is_read_access = 'read' === $access_type; + + // Check for password-protected post. + if ( + $can_access + && 'post_type' === $info['object_type'] + && $info['item_id'] + && ( + ( + $is_read_access + && pods_access_bypass_post_with_password( $info ) + ) + || ( + ! $is_read_access + && post_password_required( $info['item_id'] ) + ) + ) + ) { + $can_access = false; + } + + /** + * Allow filtering whether a user has access to an object after the normal capability check runs. + * + * @since 3.1.0 + * + * @param bool $can_access Whether a user has access to an object. + * @param int $user_id The user ID to check against. + * @param array $info { + * The normalized Pod information referenced. + * + * @type string|null $object_type The object type (if set). + * @type string|null $object_name The object name (if set). + * @type int|string|null $item_id The item ID (if set). + * @type Pods|null $pods The Pods object (if built or provided). + * @type Pod|null $pod The Pod object (if built or provided). + * } + * @param string $access_type The type of access to check for (read, add, edit, delete). + * @param string|null $context The unique slug that can be referenced by hooks for context. + * @param array $capabilities The list of capabilities used for checking access against an object. + */ + return (bool) apply_filters( + 'pods_user_can_access_object', + $can_access, + $user_id, + $info, + $access_type, + $context, + $capabilities + ); +} + +/** + * Determine whether the current user has access to an object. + * + * @since 3.1.0 + * + * @param array $args { + * The arguments to use. + * + * @type string|null $object_type The object type. + * @type string|null $object_name The object name. + * @type int|string|null $item_id The item ID. + * @type Pods|null $pods The Pods object. + * @type Pod|null $pod The Pod object. + * @type bool $build_pods Whether to try to build a Pods object from the object type/name/ID (false by default). + * @type bool $build_pod Whether to try to build a Pod object from the object type/name (false by default). + * } + * @param string $access_type The type of access to check for (read, add, edit, delete). + * @param string|null $context The unique slug that can be referenced by hooks for context. + * + * @return bool Whether the current user has access to an object. + */ +function pods_current_user_can_access_object( array $args, string $access_type = 'edit', ?string $context = null ): bool { + $user_id = null; + + if ( is_user_logged_in() ) { + $user_id = get_current_user_id(); + } + + return pods_user_can_access_object( $args, $user_id, $access_type, $context ); +} + +/** + * Build and map the capabilities that a specific object type/name/ID have in relation to a user ID. + * + * @since 3.1.0 + * + * @param array $args { + * The arguments to use. + * + * @type string|null $object_type The object type. + * @type string|null $object_name The object name. + * @type int|string|null $item_id The item ID. + * @type Pods|null $pods The Pods object. + * @type Pod|null $pod The Pod object. + * @type bool $build_pods Whether to try to build a Pods object from the object type/name/ID (false by default). + * @type bool $build_pod Whether to try to build a Pod object from the object type/name (false by default). + * } + * @param int|null $user_id The user ID accessing the object. + * @param bool $strict Whether to strictly get the capabilities or have the 'read' capability evaluate to null if it's public (defaults to false). + * + * @return array|null The capabilities that a specific object type/name/ID have in relation to a user ID, or null if invalid. + */ +function pods_access_map_capabilities( array $args, ?int $user_id = null, bool $strict = false ): ?array { + $args['build_pods'] = true; + $args['build_pod'] = true; + + $info = pods_info_from_args( $args ); + + // If no object type or name, we cannot check access. + if ( empty( $info['object_type'] ) || empty( $info['object_name'] ) ) { + return null; + } + + $wp_object = null; + + $capabilities = []; + + if ( 'post_type' === $info['object_type'] ) { + $info['item_id'] = (int) $info['item_id']; + + if ( $info['item_id'] ) { + $capabilities['read'] = 'read_post'; + $capabilities['edit'] = 'edit_post'; + $capabilities['delete'] = 'delete_post'; + } else { + $capabilities['read'] = 'read'; + $capabilities['edit'] = 'edit_posts'; + $capabilities['delete'] = 'delete_posts'; + } + + $capabilities['add'] = 'create_posts'; + $capabilities['read_private'] = 'read_private_posts'; + $capabilities['edit_others'] = 'edit_others_posts'; + $capabilities['delete_others'] = 'delete_others_posts'; + $capabilities['delete_published'] = 'delete_published_posts'; + $capabilities['delete_private'] = 'delete_private_posts'; + + // Maybe map capabilities to the post type. + $wp_object = get_post_type_object( $info['object_name'] ); + + if ( $info['item_id'] ) { + $post = get_post( $info['item_id'] ); + + // If the post was found, do fine-grained access checks. + if ( $post instanceof WP_Post ) { + $status_obj = get_post_status_object( $post->post_status ); + + // Check if the person is allowed to read other posts. + if ( + $user_id + && $post->post_author + && (int) $user_id === (int) $post->post_author + ) { + // This is their own post, they can have access. + $capabilities['read'] = 'read'; + } elseif ( + ! $status_obj + || $status_obj->private + ) { + // This is a private post, check private post capability. + $capabilities['read'] = $capabilities['read_private']; + } + } + } + } elseif ( 'taxonomy' === $info['object_type'] ) { + $info['item_id'] = (int) $info['item_id']; + + $capabilities['read'] = 'read'; + $capabilities['add'] = 'manage_terms'; + $capabilities['edit'] = 'edit_terms'; + $capabilities['delete'] = 'delete_terms'; + + // Maybe map capabilities to the post type. + $wp_object = get_taxonomy( $info['object_name'] ); + } elseif ( 'user' === $info['object_type'] ) { + $info['item_id'] = (int) $info['item_id']; + + $capabilities['read'] = 'list_users'; + $capabilities['add'] = 'create_users'; + $capabilities['edit'] = 'edit_users'; + $capabilities['delete'] = 'delete_users'; + + // If an object ID is provided, check for access for that specific user. + if ( ! empty( $info['item_id'] ) ) { + $capabilities['edit'] = 'edit_user'; + $capabilities['delete'] = 'delete_user'; + } + + // Fake the WP object for the logic below. + $wp_object = (object) [ + 'public' => false, + 'cap' => (object) [], + ]; + } elseif ( 'media' === $info['object_type'] ) { + $info['item_id'] = (int) $info['item_id']; + + $capabilities['read'] = 'read'; + $capabilities['add'] = 'upload_files'; + $capabilities['edit'] = 'upload_files'; + $capabilities['delete'] = 'upload_files'; + + // Fake the WP object for the logic below. + $wp_object = (object) [ + 'public' => false, + 'cap' => (object) [], + ]; + } elseif ( 'comment' === $info['object_type'] ) { + $info['item_id'] = (int) $info['item_id']; + + $capabilities['read'] = 'read'; + $capabilities['add'] = 1 === (int) get_option( 'comment_registration' ) ? 'read' : null; + $capabilities['edit'] = 'moderate_comments'; + $capabilities['delete'] = 'moderate_comments'; + + // If an object ID is provided, check for access for that specific user. + if ( ! empty( $info['item_id'] ) ) { + $capabilities['edit'] = 'edit_comment'; + } + + // Fake the WP object for the logic below. + $wp_object = (object) [ + 'public' => true, + 'cap' => (object) [], + ]; + } elseif ( 'settings' === $info['object_type'] ) { + $capabilities['read'] = 'manage_options'; + $capabilities['edit'] = 'manage_options'; + $capabilities['delete'] = 'manage_options'; + + // Fake the WP object for the logic below. + $wp_object = (object) [ + 'public' => false, + 'cap' => (object) [], + ]; + } elseif ( 'pod' === $info['object_type'] || 'table' === $info['object_type'] ) { + $info['item_id'] = (int) $info['item_id']; + + $capabilities['read'] = 'pods_read_' . $info['object_name']; + $capabilities['add'] = 'pods_add_' . $info['object_name']; + $capabilities['edit'] = 'pods_edit_' . $info['object_name']; + $capabilities['delete'] = 'pods_delete_' . $info['object_name']; + $capabilities['edit_others'] = 'pods_edit_others_' . $info['object_name']; + $capabilities['delete_others'] = 'pods_delete_others_' . $info['object_name']; + + $is_public = false; + + if ( $info['pods'] instanceof Pods && $info['pod'] instanceof Pod ) { + // If an object ID is provided, check for access for that specific item. + if ( $info['item_id'] && $info['pods']->exists() ) { + // Check for author field. + $author_field = $info['pod']->get_field( 'author' ); + + $author_user_id = $author_field ? (int) $info['pods']->field( $author_field->get_name() . '.ID' ) : null; + + // If we have an author field, check if they are the author. + if ( $author_field ) { + if ( $user_id && $author_user_id === $user_id ) { + // This is their own post, they can also have access if have edit access. + $capabilities['read'] = [ + $capabilities['read'], + 'pods_edit_' . $info['object_name'], + ]; + } else { + // This is not their post, check if they have access to others. + $capabilities['edit'] = 'pods_edit_others_' . $info['object_name']; + $capabilities['delete'] = 'pods_delete_others_' . $info['object_name']; + } + } + } + + $is_public = $info['pod']->get_arg( 'public', '0', true ); + $is_public = filter_var( $is_public, FILTER_VALIDATE_BOOLEAN ); + + // Fake the WP object for the logic below. + $wp_object = (object) [ + 'public' => $is_public, + 'cap' => (object) [], + ]; + } + + if ( $is_public ) { + $capabilities['read'] = 'read'; + } + } + + // If no post type object is found, we cannot check access. + if ( ! $wp_object ) { + return null; + } + + // Check if there are any capabilities mapped for this type object. + foreach ( $capabilities as $access_type => $capability ) { + if ( $capability ) { + if ( is_array( $capability ) ) { + foreach ( $capability as $k => $cap ) { + if ( isset( $wp_object->cap->{$cap} ) ) { + $capabilities[ $access_type ][ $k ] = $wp_object->cap->{$cap}; + } + } + } elseif ( isset( $wp_object->cap->{$capability} ) ) { + $capabilities[ $access_type ] = $wp_object->cap->{$capability}; + } + } + } + + // If the object is public, allow read for anyone even logged out. + if ( ! $strict && $wp_object->public && 'read' === $capabilities['read'] && ! $user_id ) { + $capabilities['read'] = null; + } + + /** + * Allow filtering the list of capabilities used for checking access against an object type or singular object. + * + * @since 3.1.0 + * + * @param array $capabilities The list of capabilities used for checking access against an object type or singular object. + * @param int $user_id The user ID to check against. + * @param array $info { + * The normalized Pod information referenced. + * + * @type string|null $object_type The object type (if set). + * @type string|null $object_name The object name (if set). + * @type int|string|null $item_id The item ID (if set). + * @type Pods|null $pods The Pods object (if built or provided). + * @type Pod|null $pod The Pod object (if built or provided). + * } + */ + return (array) apply_filters( + 'pods_access_map_capabilities', + $capabilities, + $user_id, + $info + ); +} + +/** + * Determine whether the object type/name is public. + * + * @since 3.1.0 + * + * @param array $args { + * The arguments to use. + * + * @type string|null $object_type The object type. + * @type string|null $object_name The object name. + * @type int|string|null $item_id The item ID. + * @type Pods|null $pods The Pods object. + * @type Pod|null $pod The Pod object. + * @type bool $build_pods Whether to try to build a Pods object from the object type/name/ID (false by default). + * @type bool $build_pod Whether to try to build a Pod object from the object type/name (false by default). + * } + * @param string $context The context we are checking from (defaults to shortcode). + * + * @return bool Whether the object type/name is public. + */ +function pods_is_type_public( array $args, string $context = 'shortcode' ): bool { + $args['build_pod'] = true; + + $info = pods_info_from_args( $args ); + + $is_public = true; + + $pod_has_public = null; + + $is_post_type = 'post_type' === $info['object_type']; + $is_taxonomy = 'taxonomy' === $info['object_type']; + $is_pod = 'pod' === $info['object_type']; + $is_settings_pod = 'settings' === $info['object_type']; + + $is_shortcode_context = 'shortcode' === $context; + + if ( + $info['pod'] instanceof Pod + && ( + $is_post_type + || $is_taxonomy + || $is_pod + || $is_settings_pod + ) + ) { + $is_extended = $info['pod']->is_extended(); + + if ( ! $is_extended ) { + $is_public = $info['pod']->get_arg( 'public', null, true ); + + if ( null !== $is_public ) { + $pod_has_public = true; + + $is_public = filter_var( $is_public, FILTER_VALIDATE_BOOLEAN ); + + if ( $is_post_type || $is_taxonomy ) { + $is_public = $is_public && 1 === (int) $info['pod']->get_arg( 'publicly_queryable', $is_public, true ); + } + } + } + } + + // Maybe handle looking up the visibility based on the object type. + if ( null === $pod_has_public ) { + if ( $is_post_type ) { + // If no object name is provided, we cannot check if it is public. + if ( empty( $info['object_name'] ) ) { + $is_public = false; + } else { + $post_type_object = get_post_type_object( $info['object_name'] ); + + // Post type not found. + if ( ! $post_type_object ) { + return false; + } + + $is_public = $post_type_object->public && $post_type_object->publicly_queryable; + } + } elseif ( $is_taxonomy ) { + // If no object name is provided, we cannot check if it is public. + if ( empty( $info['object_name'] ) ) { + $is_public = false; + } else { + $taxonomy_object = get_taxonomy( $info['object_name'] ); + + // Post type not found. + if ( ! $taxonomy_object ) { + return false; + } + + $is_public = $taxonomy_object->public && $taxonomy_object->publicly_queryable; + } + } elseif ( 'user' === $info['object_type'] ) { + // Users are not public for shortcodes. + if ( $is_shortcode_context ) { + $is_public = false; + } + } elseif ( $is_pod || $is_settings_pod ) { + // Pods need special default handling for shortcodes. + if ( $is_shortcode_context ) { + $first_pods_version = get_option( 'pods_framework_version_first' ); + $first_pods_version = '' === $first_pods_version ? PODS_VERSION : $first_pods_version; + + $is_public = version_compare( $first_pods_version, '3.1.0-a-1', '<' ) ? true : false; + } + } + } + + /** + * Allow filtering whether the object type/name is public. + * + * @since 3.1.0 + * + * @param bool $is_public Whether the object type/name is public. + * @param array $info { + * The normalized Pod information referenced. + * + * @type string|null $object_type The object type (if set). + * @type string|null $object_name The object name (if set). + * @type int|string|null $item_id The item ID (if set). + * @type Pods|null $pods The Pods object (if built or provided). + * @type Pod|null $pod The Pod object (if built or provided). + * } + * @param string|null $context The context we are checking from (shortcode or null). + * @param Pod|null $pod The Pod object if set. + */ + return (bool) apply_filters( + 'pods_is_type_public', + $is_public, + $info, + $context + ); +} + +/** + * Determine whether a post should be bypassed because it it has a password. + * + * @since 3.1.0 + * + * @param array $args { + * The arguments to use. + * + * @type string|null $object_type The object type. + * @type string|null $object_name The object name. + * @type int|string|null $item_id The item ID. + * @type Pods|null $pods The Pods object. + * @type Pod|null $pod The Pod object. + * @type bool $build_pods Whether to try to build a Pods object from the object type/name/ID (false by default). + * @type bool $build_pod Whether to try to build a Pod object from the object type/name (false by default). + * } + * + * @return bool Whether a post should be bypassed because it it has a password. + */ +function pods_access_bypass_post_with_password( array $args ): bool { + $info = pods_info_from_args( $args ); + + if ( 'post_type' !== $info['object_type'] || ! $info['item_id'] ) { + return false; + } + + $post = get_post( (int) $info['item_id'] ); + + if ( ! $post instanceof WP_Post ) { + return false; + } + + // Bypass posts that have a password required but not provided. + $bypass_post_with_password = post_password_required( $post ); + + /** + * Allow filtering whether a post should be bypassed because it it has a password. + * + * @since 3.1.0 + * + * @param bool $bypass_post_with_password Whether a post should be bypassed because it it has a password. + * @param array $info { + * The normalized Pod information referenced. + * + * @type string|null $object_type The object type (if set). + * @type string|null $object_name The object name (if set). + * @type int|string|null $item_id The item ID (if set). + * @type Pods|null $pods The Pods object (if built or provided). + * @type Pod|null $pod The Pod object (if built or provided). + * } + */ + return (bool) apply_filters( + 'pods_access_bypass_post_with_password', + $bypass_post_with_password, + $info + ); +} + +/** + * Determine whether a post should be bypassed because it is private and capabilities are not met. + * + * @since 3.1.0 + * + * @param array $args { + * The arguments to use. + * + * @type string|null $object_type The object type. + * @type string|null $object_name The object name. + * @type int|string|null $item_id The item ID. + * @type Pods|null $pods The Pods object. + * @type Pod|null $pod The Pod object. + * @type bool $build_pods Whether to try to build a Pods object from the object type/name/ID (false by default). + * @type bool $build_pod Whether to try to build a Pod object from the object type/name (false by default). + * } + * + * @return bool Whether a post should be bypassed because it is private and capabilities are not met. + */ +function pods_access_bypass_private_post( array $args ): bool { + $info = pods_info_from_args( $args ); + + if ( 'post_type' !== $info['object_type'] || ! $info['item_id'] ) { + return false; + } + + $post = get_post( $info['item_id'] ); + + if ( ! $post instanceof WP_Post ) { + return false; + } + + $bypass_private_post = false; + + if ( ! is_post_publicly_viewable( $post ) ) { + $bypass_private_post = ! pods_current_user_can_access_object( $info, 'read' ); + } + + /** + * Allow filtering whether a post should be bypassed because it is private. + * + * @since 3.1.0 + * + * @param bool $bypass_private_post Whether a post should be bypassed because it is private. + * @param array $info { + * The normalized Pod information referenced. + * + * @type string|null $object_type The object type (if set). + * @type string|null $object_name The object name (if set). + * @type int|string|null $item_id The item ID (if set). + * @type Pods|null $pods The Pods object (if built or provided). + * @type Pod|null $pod The Pod object (if built or provided). + * } + */ + return (bool) apply_filters( + 'pods_access_bypass_private_post', + $bypass_private_post, + $info + ); +} + +/** + * Determine whether dynamic features can be used. + * + * @since 3.1.0 + * + * @return bool Whether dynamic features can be used. + */ +function pods_can_use_dynamic_features( ?Pod $pod = null ): bool { + // Check if the constant is defined and only override if no $pod is set or dynamic features are totally disabled. + if ( + defined( 'PODS_DYNAMIC_FEATURES_ALLOW' ) + && ( + ! $pod + || ! PODS_DYNAMIC_FEATURES_ALLOW + ) + ) { + return PODS_DYNAMIC_FEATURES_ALLOW; + } + + // Check if all dynamic features are disabled. + $dynamic_features_allow = pods_get_setting( 'dynamic_features_allow', '1' ); + $dynamic_features_allow = filter_var( $dynamic_features_allow, FILTER_VALIDATE_BOOLEAN ); + + if ( $dynamic_features_allow && $pod instanceof Pod ) { + // Check if all dynamic features are disabled for the Pod. + $dynamic_features_allow = $pod->get_arg( 'dynamic_features_allow', 'inherit' ); + + if ( 'inherit' === $dynamic_features_allow ) { + $dynamic_features_allow = pods_is_type_public( + [ + 'pod' => $pod, + ] + ); + } else { + $dynamic_features_allow = filter_var( $dynamic_features_allow, FILTER_VALIDATE_BOOLEAN ); + } + } + + return $dynamic_features_allow; +} + +/** + * Determine whether any or a specific dynamic feature can be used. + * + * @since 3.1.0 + * + * @param string $type The dynamic feature type. + * + * @return bool Whether any or a specific dynamic feature can be used. + */ +function pods_can_use_dynamic_feature( string $type ): bool { + if ( ! pods_can_use_dynamic_features() ) { + return false; + } + + // Handle the constants. + if ( 'view' === $type && defined( 'PODS_SHORTCODE_ALLOW_VIEWS' ) && ! PODS_SHORTCODE_ALLOW_VIEWS ) { + return false; + } + + if ( empty( $type ) ) { + return false; + } + + $dynamic_features_enabled = (array) pods_get_setting( 'dynamic_features_enabled', [ + 'display', + 'form', + ] ); + $dynamic_features_enabled = array_filter( $dynamic_features_enabled ); + + $constant_dynamic_features_enabled = defined( 'PODS_DYNAMIC_FEATURES_ENABLED' ) ? PODS_DYNAMIC_FEATURES_ENABLED : false; + + if ( false !== $constant_dynamic_features_enabled && ! is_array( $constant_dynamic_features_enabled ) ) { + $constant_dynamic_features_enabled = explode( ',', $constant_dynamic_features_enabled ); + $constant_dynamic_features_enabled = array_filter( $constant_dynamic_features_enabled ); + + $dynamic_features_enabled = $constant_dynamic_features_enabled; + } + + if ( empty( $dynamic_features_enabled ) ) { + return false; + } + + return in_array( $type, $dynamic_features_enabled, true ); +} + +/** + * Determine whether specific dynamic feature is unrestricted. + * + * @since 3.1.0 + * + * @param array $args { + * The arguments to use. + * + * @type string|null $object_type The object type. + * @type string|null $object_name The object name. + * @type int|string|null $item_id The item ID. + * @type Pods|null $pods The Pods object. + * @type Pod|null $pod The Pod object. + * @type bool $build_pods Whether to try to build a Pods object from the object type/name/ID (false by default). + * @type bool $build_pod Whether to try to build a Pod object from the object type/name (false by default). + * } + * @param string $type The dynamic feature type. + * @param string $mode The dynamic feature mode (like "add" or "edit" for the form feature). + * + * @return bool Whether specific dynamic feature is unrestricted. + */ +function pods_can_use_dynamic_feature_unrestricted( array $args, string $type, ?string $mode = null ): bool { + if ( ! pods_can_use_dynamic_feature( $type ) ) { + return false; + } + + if ( + defined( 'PODS_DYNAMIC_FEATURES_RESTRICT' ) + && ! PODS_DYNAMIC_FEATURES_RESTRICT + ) { + $can_use_unrestricted = true; + } else { + $can_use_unrestricted = false; + + $args['build_pod'] = true; + + $info = pods_info_from_args( $args ); + + if ( ! $info['pod'] ) { + $can_use_unrestricted = false; + } else { + $is_public_content_type = pods_is_type_public( $info ); + + $default_restricted_dynamic_features = [ + 'form', + ]; + + if ( ! $is_public_content_type ) { + $default_restricted_dynamic_features[] = 'display'; + } + + $default_restricted_dynamic_features_forms = [ + 'edit', + ]; + + if ( ! $is_public_content_type ) { + $default_restricted_dynamic_features_forms[] = 'add'; + } + + // Check if all dynamic features are unrestricted. + $restrict_dynamic_features = $info['pod']->get_arg( 'restrict_dynamic_features', '1' ); + $restrict_dynamic_features = filter_var( $restrict_dynamic_features, FILTER_VALIDATE_BOOLEAN ); + + if ( ! $restrict_dynamic_features ) { + $can_use_unrestricted = true; + } elseif ( ! empty( $type ) ) { + if ( defined( 'PODS_DYNAMIC_FEATURES_RESTRICTED' ) && false !== PODS_DYNAMIC_FEATURES_RESTRICTED ) { + $constant_restricted_dynamic_features = PODS_DYNAMIC_FEATURES_RESTRICTED; + + if ( ! is_array( $constant_restricted_dynamic_features ) ) { + $constant_restricted_dynamic_features = explode( ',', $constant_restricted_dynamic_features ); + } + + $restricted_dynamic_features = $constant_restricted_dynamic_features; + } else { + $restricted_dynamic_features = (array) $info['pod']->get_arg( 'restricted_dynamic_features', $default_restricted_dynamic_features ); + } + + $restricted_dynamic_features = array_filter( $restricted_dynamic_features ); + + if ( empty( $restricted_dynamic_features ) ) { + $can_use_unrestricted = true; + } else { + $can_use_unrestricted = ! in_array( $type, $restricted_dynamic_features, true ); + } + + if ( ! $can_use_unrestricted && 'form' === $type && $mode ) { + if ( defined( 'PODS_DYNAMIC_FEATURES_RESTRICTED_FORMS' ) && false !== PODS_DYNAMIC_FEATURES_RESTRICTED_FORMS ) { + $constant_restricted_dynamic_features_forms = PODS_DYNAMIC_FEATURES_RESTRICTED_FORMS; + + if ( ! is_array( $constant_restricted_dynamic_features_forms ) ) { + $constant_restricted_dynamic_features_forms = explode( ',', $constant_restricted_dynamic_features_forms ); + } + + $restricted_dynamic_features_forms = $constant_restricted_dynamic_features_forms; + } else { + $restricted_dynamic_features_forms = (array) $info['pod']->get_arg( 'restricted_dynamic_features_forms', $default_restricted_dynamic_features_forms ); + } + + $restricted_dynamic_features_forms = array_filter( $restricted_dynamic_features_forms ); + + if ( empty( $restricted_dynamic_features_forms ) ) { + $can_use_unrestricted = true; + } else { + $can_use_unrestricted = ! in_array( $mode, $restricted_dynamic_features_forms, true ); + } + } + } + } + } + + return $can_use_unrestricted; +} + +/** + * Get the access notice for admin user based on object type and object name. + * + * @since 3.1.0 + * + * @param array $args { + * The arguments to use. + * + * @type string|null $object_type The object type. + * @type string|null $object_name The object name. + * @type int|string|null $item_id The item ID. + * @type Pods|null $pods The Pods object. + * @type Pod|null $pod The Pod object. + * @type bool $build_pods Whether to try to build a Pods object from the object type/name/ID (false by default). + * @type bool $build_pod Whether to try to build a Pod object from the object type/name (false by default). + * } + * @param bool $force_message Whether to force the message to show even if messages are hidden by a setting. + * + * @return string The access notice for admin user based on object type and object name. + */ +function pods_get_access_admin_notice( array $args, bool $force_message = false ): string { + $args['build_pod'] = true; + + $info = pods_info_from_args( $args ); + + $identifier_for_html = esc_html( json_encode( [ + 'object_type' => $info['object_type'], + 'object_name' => $info['object_name'], + 'item_id' => $info['item_id'], + ] ) ); + + // Check if constant is hiding all notices. + if ( ! $force_message && defined( 'PODS_ACCESS_HIDE_NOTICES' ) && PODS_ACCESS_HIDE_NOTICES ) { + return ''; + } + + // Check notice setting for the Pod itself. + if ( $info['pod'] instanceof Pod ) { + $show_access_admin_notices_for_pod = $info['pod']->get_arg( 'show_access_admin_notices', 'inherit' ); + + if ( 'inherit' !== $show_access_admin_notices_for_pod ) { + $show_access_admin_notices_for_pod = filter_var( $show_access_admin_notices_for_pod, FILTER_VALIDATE_BOOLEAN ); + + // Check if all notices have been dismissed for the pod. + if ( ! $force_message && ! $show_access_admin_notices_for_pod ) { + return ''; + } + } + } + + // Show notice that this content may not be visible to others. + $show_access_admin_notices = pods_get_setting( 'show_access_admin_notices', true ); + $show_access_admin_notices = filter_var( $show_access_admin_notices, FILTER_VALIDATE_BOOLEAN ); + + // Check if all notices have been dismissed. + if ( ! $force_message && ! $show_access_admin_notices ) { + return ''; + } + + $summary = esc_html__( 'Pods Access Rights: Admin-only Notice', 'pods' ); + + $content = sprintf( + ' +

    + %1$s +
    + + %3$s + | %5$s + +

    + ', + esc_html__( 'The content below is not public and may not be available to everyone else.', 'pods' ), + esc_url( 'https://docs.pods.io/displaying-pods/access-rights-in-pods/' ), + esc_html__( 'How access rights work with Pods (Documentation)', 'pods' ), + esc_url( admin_url( 'admin.php?page=pods-settings#heading-security' ) ), + esc_html__( 'Edit other access right options', 'pods' ) + ); + + return '' + . pods_message( + sprintf( + ' +
    + %1$s + %2$s +
    + ', + strip_tags( ! empty( $info['summary'] ) ? $info['summary'] : $summary ), + ! empty( $info['content'] ) ? wpautop( $info['content'] ) : $content + ), + 'notice', + true + ); +} + +/** + * Get the access notice for non-admin user based on object type and object name. + * + * @since 3.1.0 + * + * @param array $args { + * The arguments to use. + * + * @type string|null $object_type The object type. + * @type string|null $object_name The object name. + * @type int|string|null $item_id The item ID. + * @type Pods|null $pods The Pods object. + * @type Pod|null $pod The Pod object. + * @type bool $build_pods Whether to try to build a Pods object from the object type/name/ID (false by default). + * @type bool $build_pod Whether to try to build a Pod object from the object type/name (false by default). + * } + * @param bool $force_message Whether to force the message to show even if messages are hidden by a setting. + * @param string|null $message A custom message to use for the notice text. + * + * @return string The access notice for non-admin user based on object type and object name. + */ +function pods_get_access_user_notice( array $args, bool $force_message = false, ?string $message = null ): string { + $args['build_pod'] = true; + + $info = pods_info_from_args( $args ); + + $identifier_for_html = esc_html( json_encode( [ + 'object_type' => $info['object_type'], + 'object_name' => $info['object_name'], + 'item_id' => $info['item_id'], + ] ) ); + + // Check for password-protected post. + if ( $info['item_id'] && pods_access_bypass_post_with_password( $info ) ) { + $message = get_the_password_form( $info['item_id'] ); + + return '' + . pods_message( + sprintf( + '

    %1$s

    %2$s', + esc_html__( 'Access Restricted', 'pods' ), + $message + ), + 'error', + true + ); + } + + // Check if constant is hiding all notices. + if ( ! $force_message && defined( 'PODS_ACCESS_HIDE_NOTICES' ) && PODS_ACCESS_HIDE_NOTICES ) { + return ''; + } + + // Check notice setting for the Pod itself. + if ( $info['pod'] instanceof Pod ) { + $show_access_restricted_messages_for_pod = $info['pod']->get_arg( 'show_access_restricted_messages', 'inherit' ); + + if ( 'inherit' !== $show_access_restricted_messages_for_pod ) { + $show_access_restricted_messages_for_pod = filter_var( $show_access_restricted_messages_for_pod, FILTER_VALIDATE_BOOLEAN ); + + // Check if all notices have been dismissed for the pod. + if ( ! $force_message && ! $show_access_restricted_messages_for_pod ) { + return ''; + } + } + } + + // Show notice that this content may not be visible to others. + $show_access_restricted_messages = pods_get_setting( 'show_access_restricted_messages', false ); + $show_access_restricted_messages = filter_var( $show_access_restricted_messages, FILTER_VALIDATE_BOOLEAN ); + + // Check if all notices have been dismissed. + if ( ! $force_message && ! $show_access_restricted_messages ) { + return ''; + } + + $message = $message ?? esc_html__( 'You do not have access to this embedded content.', 'pods' ); + + return '' + . pods_message( + sprintf( + '

    %1$s: %2$s

    ', + esc_html__( 'Access Restricted', 'pods' ), + $message + ), + 'error', + true + ); +} + +/** + * Determine whether SQL clauses can be used with dynamic features. + * + * @since 3.1.0 + * + * @param null|string $clause_type The clause type to check if allowed, if null used then it checks if any clauses are allowed. + * + * @return bool Whether SQL clauses can be used with dynamic features. + */ +function pods_can_use_dynamic_feature_sql_clauses( ?string $clause_type = null ): bool { + if ( defined( 'PODS_DISABLE_SHORTCODE_SQL' ) ) { + // Negate the check since this is a "disable" constant. + return ! PODS_DISABLE_SHORTCODE_SQL; + } + + if ( defined( 'PODS_DYNAMIC_FEATURES_ALLOW_SQL_CLAUSES' ) ) { + $allow_sql_clauses = PODS_DYNAMIC_FEATURES_ALLOW_SQL_CLAUSES; + } else { + $first_pods_version = get_option( 'pods_framework_version_first' ); + $first_pods_version = '' === $first_pods_version ? PODS_VERSION : $first_pods_version; + + $allow_sql_clauses = pods_get_setting( 'dynamic_features_allow_sql_clauses', version_compare( $first_pods_version, '3.1.0-a-1', '<' ) ? 'simple' : '0' ); + } + + if ( + false === $allow_sql_clauses + || '0' === $allow_sql_clauses + ) { + return false; + } + + if ( null === $clause_type ) { + return true; + } + + if ( 'simple' === $clause_type && 'all' === $allow_sql_clauses ) { + return true; + } + + return $clause_type === $allow_sql_clauses; +} + +/** + * Determine whether a callback can be used. + * + * @since 3.1.0 + * + * @param string|callable $callback The callback to check. + * @param array $params Parameters used by Pods::helper() method. + * + * @return bool Whether the callback can be used. + */ +function pods_access_callback_allowed( $callback, array $params = [] ): bool { + // Real callables are allowed because they are done through PHP calls. + if ( ! is_string( $callback ) ) { + return true; + } + + if ( ! pods_can_use_dynamic_feature( 'display' ) ) { + return false; + } + + if ( + defined( 'PODS_DISPLAY_CALLBACKS' ) + && ! PODS_DISPLAY_CALLBACKS + ) { + return false; + } + + /** + * Allows changing whether callbacks are allowed to run. + * + * @param bool $allow_callbacks Whether callbacks are allowed to run. + * @param array $params Parameters used by Pods::helper() method. + * + * @since 2.8.0 + */ + $allow_callbacks = (bool) apply_filters( 'pods_helper_allow_callbacks', true, $params ); + + if ( ! $allow_callbacks ) { + return false; + } + + $disallowed = [ + // Regex related. + 'preg_replace', + 'preg_replace_array', + 'preg_replace_callback', + 'preg_replace_callback_array', + 'preg_match', + 'preg_match_all', + // Shell/Eval related. + 'system', + 'exec', + 'passthru', + 'proc_close', + 'proc_get_status', + 'proc_nice', + 'proc_open', + 'proc_terminate', + 'shell_exec', + 'system', + 'eval', + 'create_function', + // File related. + 'popen', + 'include', + 'include_once', + 'require', + 'require_once', + 'file_get_contents', + 'file_put_contents', + 'get_template_part', + // Nonce related. + 'wp_nonce_url', + 'wp_nonce_field', + 'wp_create_nonce', + 'check_admin_referer', + 'check_ajax_referer', + 'wp_verify_nonce', + // PHP related. + 'constant', + 'defined', + 'get_current_user', + 'get_defined_constants', + 'get_defined_functions', + 'get_defined_vars', + 'get_extension_funcs', + 'get_include_path', + 'get_included_files', + 'get_loaded_extensions', + 'get_required_files', + 'get_resources', + 'getenv', + 'getopt', + 'ini_alter', + 'ini_get', + 'ini_get_all', + 'ini_restore', + 'ini_set', + 'php_ini_loaded_file', + 'php_ini_scanned_files', + 'php_sapi_name', + 'php_uname', + 'phpinfo', + 'phpversion', + 'putenv', + // WordPress related. + 'get_userdata', + 'get_currentuserinfo', + 'get_post', + 'get_term', + 'get_comment', + ]; + + $allowed = []; + + if ( defined( 'PODS_DISPLAY_CALLBACKS' ) ) { + $display_callbacks = PODS_DISPLAY_CALLBACKS; + } else { + $first_pods_version = get_option( 'pods_framework_version_first' ); + $first_pods_version = '' === $first_pods_version ? PODS_VERSION : $first_pods_version; + + $display_callbacks = pods_get_setting( 'display_callbacks', version_compare( $first_pods_version, '3.1.0-a-1', '<' ) ? 'restricted' : 'customized' ); + } + + if ( '0' === $display_callbacks ) { + return false; + } + + // Maybe specify the list of allowed callbacks. + if ( 'customized' === $display_callbacks ) { + if ( defined( 'PODS_DISPLAY_CALLBACKS_ALLOWED' ) ) { + $display_callbacks_allowed = PODS_DISPLAY_CALLBACKS_ALLOWED; + } else { + // Maybe specify the list of allowed callbacks + $display_callbacks_allowed = pods_get_setting( 'display_callbacks_allowed', 'esc_attr,esc_html' ); + } + + if ( ! is_array( $display_callbacks_allowed ) ) { + $display_callbacks_allowed = str_replace( "\n", ',', $display_callbacks_allowed ); + $display_callbacks_allowed = explode( ',', $display_callbacks_allowed ); + } + + $display_callbacks_allowed = array_map( 'trim', $display_callbacks_allowed ); + $display_callbacks_allowed = array_filter( $display_callbacks_allowed ); + + if ( ! empty( $display_callbacks_allowed ) ) { + $allowed = $display_callbacks_allowed; + } + } + + /** + * Allows adjusting the disallowed callbacks as needed. + * + * @param array $disallowed List of callbacks not allowed. + * @param array $params Parameters used by Pods::helper() method. + * + * @since 2.7.0 + */ + $disallowed = apply_filters( 'pods_helper_disallowed_callbacks', $disallowed, $params ); + + /** + * Allows adjusting the allowed callbacks as needed. + * + * @param array $allowed List of callbacks explicitly allowed. + * @param array $params Parameters used by Pods::helper() method. + * + * @since 2.7.0 + */ + $allowed = apply_filters( 'pods_helper_allowed_callbacks', $allowed, $params ); + + // Clean up helper callback (if string). + if ( is_string( $callback ) ) { + $callback = strip_tags( str_replace( array( '`', chr( 96 ) ), "'", $callback ) ); + } + + return ( + ! in_array( $callback, $disallowed, true ) + && ( + empty( $allowed ) + || in_array( $callback, $allowed, true ) + ) + ); +} + +/** + * Get the pod access tab options for a specific pod. + * + * @since 3.1.0 + * + * @param string $pod_type The pod type. + * @param string $pod_name The pod name. + * @param null|Pod $pod The pod object. + * + * @return array The pod access tab options for a specific pod. + */ +function pods_access_pod_options( string $pod_type, string $pod_name, ?Pod $pod = null ): array { + $first_pods_version = get_option( 'pods_framework_version_first' ); + $first_pods_version = '' === $first_pods_version ? PODS_VERSION : $first_pods_version; + + $options = []; + + $options['security_access_rights_info'] = [ + 'label' => __( 'How access rights work in Pods', 'pods' ), + 'type' => 'html', + 'html_content' => sprintf( + ' +

    %1$s

    +

    %2$s

    + ', + __( 'Pods handles access rights similar to how WordPress itself works.', 'pods' ), + __( 'Read more about how access rights work in Pods on our Documentation site', 'pods' ) + ), + ]; + + if ( 'pod' === $pod_type ) { + $options['public'] = [ + 'label' => __( 'Public', 'pods' ), + 'help' => __( 'You can still embed Pods Content and Forms through PHP and make use of other features directly through code.', 'pods' ), + 'description' => __( 'When a content type is public, it can be viewed by anyone when it is embedded through Dynamic Features. Otherwise, a user will need to have the corresponding "read" capability for the content type.', 'pods' ), + 'type' => 'boolean', + 'default' => version_compare( $first_pods_version, '3.1.0-a-1', '<' ) ? true : false, + 'boolean_yes_label' => '', + ]; + } + + if ( pods_can_use_dynamic_features() ) { + $options['dynamic_features_allow'] = [ + 'label' => __( 'Dynamic Features', 'pods' ), + 'help' => [ + __( 'Enabling Dynamic Features will also enable the additional access rights checks for user access. This ensures that people viewing embedded content and forms have the required capabilties. Even when Dynamic Features are disabled, you can still embed Pods Content and Forms through PHP and make use of other features directly through code.', 'pods' ), + 'https://docs.pods.io/displaying-pods/access-rights-in-pods/', + ], + 'description' => __( 'Dynamic features include Pods Shortcodes, Blocks, and Widgets which let you embed content and forms on your site.', 'pods' ), + 'type' => 'pick', + 'default' => 'inherit', + 'pick_format_type' => 'single', + 'pick_format_single' => 'radio', + 'data' => [ + 'inherit' => __( 'WP Default - If the content type is marked "Public" with WordPress then Dynamic Features will be enabled.', 'pods' ), + '1' => __( 'Enable Dynamic Features including Pods Shortcodes, Blocks, and Widgets for this content type', 'pods' ), + '0' => __( 'Disable All Dynamic Features in Pods for this content type', 'pods' ), + ], + 'dependency' => true, + ]; + + $is_public_content_type = pods_is_type_public( + [ + 'pod' => $pod, + ] + ); + + $options['restrict_dynamic_features'] = [ + 'label' => __( 'Restrict Dynamic Features', 'pods' ), + 'help' => [ + __( 'This will check access rights for whether someone should have access to specific content before a they can view, modify, or interact with that content.', 'pods' ), + 'https://docs.pods.io/displaying-pods/access-rights-in-pods/', + ], + 'description' => sprintf( + '%1$s %2$s', + esc_html__( 'Warning:', 'pods' ), + esc_html__( 'If you have authors/contributors on your site then disabling this would give them access to embedding content/forms without access checks for them or whoever views the embeds on the front of your site. Caution is always advised before giving access to other users you may not trust.', 'pods' ) + ), + 'type' => 'pick', + 'default' => '1', + 'pick_format_type' => 'single', + 'pick_format_single' => 'radio', + 'data' => [ + '0' => __( 'Unrestricted - Do not check for access rights for embedded content (only use this if you trust ALL users who have access to create content)', 'pods' ), + '1' => __( 'Restricted - Check access rights for embedded content', 'pods' ), + ], + 'excludes-on' => [ 'dynamic_features_allow' => '0' ], + ]; + + $default_restricted_dynamic_features = [ + 'form', + ]; + + if ( ! $is_public_content_type ) { + $default_restricted_dynamic_features[] = 'display'; + } + + $options['restricted_dynamic_features'] = [ + 'label' => __( 'Dynamic Features to Restrict', 'pods' ), + 'help' => [ + __( 'This will check access rights for the dynamic feature for whether someone should have access to specific content before a they can view, modify, or interact with that content.', 'pods' ), + 'https://docs.pods.io/displaying-pods/access-rights-in-pods/', + ], + 'type' => 'pick', + 'default' => $default_restricted_dynamic_features, + 'pick_format_type' => 'multi', + 'pick_format_multi' => 'checkbox', + 'data' => [ + 'display' => __( 'Restricted Display - Shortcodes and Blocks that allow querying content from this Pod and displaying any field will check access rights.', 'pods' ), + 'form' => __( 'Restricted Forms - The Form Shortcode and Block submitting new content or editing existing content will check access rights.', 'pods' ), + ], + 'depends-on' => [ 'restrict_dynamic_features' => '1' ], + 'excludes-on' => [ 'dynamic_features_allow' => '0' ], + ]; + + $default_restricted_dynamic_features_forms = [ + 'edit', + ]; + + if ( ! $is_public_content_type ) { + $default_restricted_dynamic_features_forms[] = 'add'; + } + + $options['restricted_dynamic_features_forms'] = [ + 'label' => __( 'Dynamic Features to Restrict for Forms', 'pods' ), + 'help' => [ + __( 'This will check access rights for whether someone should have access to specific content before a they can add or edit content.', 'pods' ), + 'https://docs.pods.io/displaying-pods/access-rights-in-pods/', + ], + 'type' => 'pick', + 'default' => $default_restricted_dynamic_features_forms, + 'pick_format_type' => 'multi', + 'pick_format_multi' => 'checkbox', + 'data' => [ + 'add' => __( 'Restricted Add New Forms - Embedding the Form Shortcode and Block to allow for adding new content will check access rights.', 'pods' ), + 'edit' => __( 'Restricted Edit Forms - Embedding the Form Shortcode and Block to allow for editing existing content will check access rights.', 'pods' ), + ], + 'depends-on-multi' => [ 'restricted_dynamic_features' => 'form' ], + 'excludes-on' => [ 'dynamic_features_allow' => '0' ], + ]; + + $options['show_access_restricted_messages'] = [ + 'label' => __( 'Access-related Restricted Messages', 'pods' ), + 'help' => [ + __( 'Access-related Restricted Messages will show to anyone who does not have access to add/edit/read a specific item from a content type.', 'pods' ), + 'https://docs.pods.io/displaying-pods/access-rights-in-pods/', + ], + 'type' => 'pick', + 'default' => 'inherit', + 'pick_format_type' => 'single', + 'pick_format_single' => 'radio', + 'data' => [ + '1' => __( 'Enable access-related restricted messages for forms/content displayed (instead of the form/content output)', 'pods' ), + '0' => __( 'Disable access-related restricted messages for forms/content displayed (the form/content output will be blank)', 'pods' ), + 'inherit' => __( 'Default - Use the global Pods setting for this', 'pods' ), + ], + 'depends-on' => [ 'restrict_dynamic_features' => '1' ], + 'excludes-on' => [ 'dynamic_features_allow' => '0' ], + ]; + + $options['show_access_admin_notices'] = [ + 'label' => __( 'Access-related Admin Notices', 'pods' ), + 'help' => [ + __( 'Access-related Admin Notices will only show to admins and will appear above content/forms that may not be entirely public.', 'pods' ), + 'https://docs.pods.io/displaying-pods/access-rights-in-pods/', + ], + 'type' => 'pick', + 'default' => 'inherit', + 'pick_format_type' => 'single', + 'pick_format_single' => 'radio', + 'data' => [ + '1' => __( 'Enable access-related admin notices above forms/content displayed', 'pods' ), + '0' => __( 'Disable access-related admin notices above forms/content displayed', 'pods' ), + 'inherit' => __( 'Default - Use the global Pods setting for this', 'pods' ), + ], + 'depends-on' => [ 'restrict_dynamic_features' => '1' ], + 'excludes-on' => [ 'dynamic_features_allow' => '0' ], + ]; + } + + $options['security_access_rights_preview'] = [ + 'label' => __( 'Capabilities preview', 'pods' ), + 'type' => 'html', + 'html_content' => ' +

    ' . esc_html__( 'Below is a list of capabilities that a user will normally need for this content.' ) . '

    + ' . pods_access_get_capabilities_preview( $pod_type, $pod_name ), + ]; + + return $options; +} + +/** + * Get the list of dynamic features allow options. + * + * @since 3.1.0 + * + * @return array The list of dynamic features allow options. + */ +function pods_access_get_dynamic_features_allow_options(): array { + return [ + 'inherit' => __( 'WP Default (if content type is Public)', 'pods' ), + '1' => __( 'Enabled', 'pods' ), + '0' => '🔒 ' . __( 'Disabled', 'pods' ), + ]; +} + +/** + * Get the list of restricted dynamic features options. + * + * @since 3.1.0 + * + * @return array The list of restricted dynamic features options. + */ +function pods_access_get_restricted_dynamic_features_options(): array { + return [ + 'display' => '🔒 ' . __( 'Display', 'pods' ), + 'form' => '🔒 ' . __( 'Form', 'pods' ), + ]; +} + +/** + * Get the access rights capabilities preview HTML. + * + * @since 3.1.0 + * + * @param string $pod_type The pod type. + * @param string $pod_name The pod name. + * + * @return string The access rights capabilities preview HTML. + */ +function pods_access_get_capabilities_preview( string $pod_type, string $pod_name ): string { + $capabilities = pods_access_map_capabilities( + [ + 'object_type' => $pod_type, + 'object_name' => $pod_name, + ], + null, + true + ); + + if ( null === $capabilities ) { + $capabilities = [ + 'read' => null, + 'add' => null, + 'edit' => null, + 'delete' => null, + ]; + } + + $capabilities_preview = [ + 'read' => esc_html__( 'Read capability', 'pods' ), + 'add' => esc_html__( 'Add New capability', 'pods' ), + 'edit' => esc_html__( 'Edit capability', 'pods' ), + 'delete' => esc_html__( 'Delete capability', 'pods' ), + 'read_private' => esc_html__( 'Read Private capability', 'pods' ), + 'edit_others' => esc_html__( 'Edit Others capability', 'pods' ), + 'delete_others' => esc_html__( 'Delete Others capability', 'pods' ), + 'delete_published' => esc_html__( 'Delete Published capability', 'pods' ), + 'delete_private' => esc_html__( 'Delete Private capability', 'pods' ), + ]; + + $capabilities_preview_list = [ + '' . $capabilities_preview['read'] . ': ' . ( $capabilities['read'] ?: __( 'Not restricted', 'pods' ) ), + ]; + + if ( 'settings' !== $pod_type ) { + $capabilities_preview_list[] = '' . $capabilities_preview['add'] . ': ' . ( $capabilities['add'] ?: __( 'Not restricted', 'pods' ) ); + } + + $capabilities_preview_list[] = '' . $capabilities_preview['edit'] . ': ' . ( $capabilities['edit'] ?: __( 'Not restricted', 'pods' ) ); + + if ( 'settings' !== $pod_type ) { + $capabilities_preview_list[] = '' . $capabilities_preview['delete'] . ': ' . ( $capabilities['delete'] ?: __( 'Not restricted', 'pods' ) ); + } + + if ( $capabilities && array_key_exists( 'read_private', $capabilities ) ) { + $capabilities_preview_list[] = '' . $capabilities_preview['read_private'] . ': ' . ( $capabilities['read_private'] ?: __( 'Not restricted', 'pods' ) ); + } + + if ( $capabilities && array_key_exists( 'edit_others', $capabilities ) ) { + $capabilities_preview_list[] = '' . $capabilities_preview['edit_others'] . ': ' . ( $capabilities['edit_others'] ?: __( 'Not restricted', 'pods' ) ); + } + + if ( $capabilities && array_key_exists( 'delete_others', $capabilities ) ) { + $capabilities_preview_list[] = '' . $capabilities_preview['delete_others'] . ': ' . ( $capabilities['delete_others'] ?: __( 'Not restricted', 'pods' ) ); + } + + if ( $capabilities && array_key_exists( 'delete_published', $capabilities ) ) { + $capabilities_preview_list[] = '' . $capabilities_preview['delete_published'] . ': ' . ( $capabilities['delete_published'] ?: __( 'Not restricted', 'pods' ) ); + } + + if ( $capabilities && array_key_exists( 'delete_private', $capabilities ) ) { + $capabilities_preview_list[] = '' . $capabilities_preview['delete_private'] . ': ' . ( $capabilities['delete_private'] ?: __( 'Not restricted', 'pods' ) ); + } + + return ' +
      +
    • ' . implode( '
    • ', $capabilities_preview_list ) . '
    • +
    + '; +} + +/** + * Get the pod settings config for access-related settings. + * + * @since 3.1.0 + * + * @return array The pod settings config for access-related settings. + */ +function pods_access_settings_config(): array { + $first_pods_version = get_option( 'pods_framework_version_first' ); + $first_pods_version = '' === $first_pods_version ? PODS_VERSION : $first_pods_version; + + $fields = []; + + $fields['dynamic_features_allow'] = [ + 'name' => 'dynamic_features_allow', + 'label' => __( 'Dynamic Features', 'pods' ), + 'help' => [ + __( 'Enabling Dynamic Features will also enable the additional access rights checks for user access. This ensures that people viewing embedded content and forms have the required capabilties. Even when Dynamic Features are disabled, you can still embed Pods Content and Forms through PHP and make use of other features directly through code.', 'pods' ), + 'https://docs.pods.io/displaying-pods/access-rights-in-pods/', + ], + 'description' => __( 'Dynamic features include Pods Shortcodes, Blocks, and Widgets which let you embed content and forms on your site.', 'pods' ), + 'type' => 'pick', + 'default' => '1', + 'pick_format_type' => 'single', + 'pick_format_single' => 'radio', + 'data' => [ + '1' => __( 'Enable Dynamic Features including Pods Shortcodes, Blocks, and Widgets', 'pods' ), + '0' => __( 'Disable All Dynamic Features in Pods', 'pods' ), + ], + 'site_health_data' => [ + '1' => __( 'Enable', 'pods' ), + '0' => __( 'Disable', 'pods' ), + ], + 'site_health_include_in_info' => true, + ]; + + $fields['security_access_rights_info'] = [ + 'name' => 'security_access_rights_info', + 'label' => __( 'How access rights work in Pods', 'pods' ), + 'type' => 'html', + 'html_content' => sprintf( + ' +

    %1$s

    +

    %2$s

    + ', + __( 'Pods handles access rights similar to how WordPress itself works.', 'pods' ), + __( 'Read more about how access rights work in Pods on our Documentation site', 'pods' ) + ), + 'depends-on' => [ 'dynamic_features_allow' => '1' ], + ]; + + $fields['dynamic_features_enabled'] = [ + 'name' => 'dynamic_features_enabled', + 'label' => __( 'Dynamic Features to Enable', 'pods' ), + 'help' => [ + __( 'You can choose one or more dynamic features to enable. By default, only Display and Form are enabled.', 'pods' ), + 'https://docs.pods.io/displaying-pods/access-rights-in-pods/', + ], + 'type' => 'pick', + 'default' => [ + 'display', + 'form', + ], + 'pick_format_type' => 'multi', + 'pick_format_multi' => 'checkbox', + 'data' => [ + 'display' => __( 'Display - Shortcodes and Blocks that allow querying content from *any* Pod and displaying any field (WordPress access rights are still checked).', 'pods' ), + 'form' => __( 'Form - The Form Shortcode and Block that allows submitting new content or editing existing content from *any* Pod (WordPress access rights are still checked).', 'pods' ), + 'view' => __( 'View - The View Shortcode and Block that allows embedding *any* theme file on a page.', 'pods' ), + ], + 'site_health_data' => [ + 'display' => __( 'Display', 'pods' ), + 'form' => __( 'Form', 'pods' ), + 'view' => __( 'View', 'pods' ), + ], + 'depends-on' => [ 'dynamic_features_allow' => '1' ], + 'site_health_include_in_info' => true, + ]; + + $fields['show_access_restricted_messages'] = [ + 'name' => 'show_access_restricted_messages', + 'label' => __( 'Access-related Restricted Messages', 'pods' ), + 'help' => [ + __( 'Access-related Restricted Messages will show to anyone who does not have access to add/edit/read a specific item from a content type.', 'pods' ), + 'https://docs.pods.io/displaying-pods/access-rights-in-pods/', + ], + 'type' => 'pick', + 'default' => '0', + 'pick_format_type' => 'single', + 'pick_format_single' => 'radio', + 'data' => [ + '1' => __( 'Enable access-related restricted messages for forms/content displayed (instead of the form/content output)', 'pods' ), + '0' => __( 'Disable access-related restricted messages for forms/content displayed (the form/content output will be blank)', 'pods' ), + ], + 'site_health_data' => [ + '1' => __( 'Enable', 'pods' ), + '0' => __( 'Disable', 'pods' ), + ], + 'site_health_include_in_info' => true, + 'depends-on' => [ 'dynamic_features_allow' => '1' ], + ]; + + $fields['show_access_admin_notices'] = [ + 'name' => 'show_access_admin_notices', + 'label' => __( 'Access-related Admin Notices', 'pods' ), + 'help' => [ + __( 'Access-related Admin Notices will only show to admins and will appear above content/forms that may not be entirely public.', 'pods' ), + 'https://docs.pods.io/displaying-pods/access-rights-in-pods/', + ], + 'type' => 'pick', + 'default' => '1', + 'pick_format_type' => 'single', + 'pick_format_single' => 'radio', + 'data' => [ + '1' => __( 'Enable access-related admin notices above forms/content displayed', 'pods' ), + '0' => __( 'Disable access-related admin notices above forms/content displayed', 'pods' ), + ], + 'site_health_data' => [ + '1' => __( 'Enable', 'pods' ), + '0' => __( 'Disable', 'pods' ), + ], + 'site_health_include_in_info' => true, + 'depends-on' => [ 'dynamic_features_allow' => '1' ], + ]; + + $fields['dynamic_features_allow_sql_clauses'] = [ + 'name' => 'dynamic_features_allow_sql_clauses', + 'label' => __( 'Allow SQL clauses to be used in Dynamic Features', 'pods' ), + 'description' => __( 'SQL clauses in general should only be enabled for sites with trusted users. Since WordPress allows anyone to enter any shortcode or block in the editor, any person with the Contributor role or higher could have access to use this.', 'pods' ), + 'type' => 'pick', + 'default' => version_compare( $first_pods_version, '3.1.0-a-1', '<' ) ? 'simple' : '0', + 'pick_format_type' => 'single', + 'pick_format_single' => 'radio', + 'data' => [ + 'all' => __( 'Unrestricted - Enable ALL SQL clause usage through dynamic features (only use this if you trust ALL users who have access to create content)', 'pods' ), + 'simple' => __( 'Restricted - Enable Simple SQL clause usage (only SELECT, WHERE, and ORDER BY) through dynamic features (only use this if you trust ALL users who have access to create content)', 'pods' ), + '0' => __( 'Disable SQL clause usage through dynamic features', 'pods' ), + ], + 'site_health_data' => [ + 'all' => __( 'Unrestricted', 'pods' ), + 'simple' => __( 'Restricted', 'pods' ), + '0' => __( 'Disable', 'pods' ), + ], + 'depends-on' => [ + 'dynamic_features_allow' => '1', + ], + 'depends-on-multi' => [ + 'dynamic_features_enabled' => 'display', + ], + 'site_health_include_in_info' => true, + ]; + + $fields['display_callbacks'] = [ + 'name' => 'display_callbacks', + 'label' => __( 'Display callbacks', 'pods' ), + 'description' => __( 'Callbacks can be used when using Pods Templating syntax like {@my_field,my_callback} in your magic tags.', 'pods' ), + 'type' => 'pick', + 'default' => version_compare( $first_pods_version, '3.1.0-a-1', '<' ) ? 'restricted' : 'customized', + 'pick_format_type' => 'single', + 'pick_format_single' => 'radio', + 'data' => [ + 'restricted' => __( 'Restricted - Certain system PHP functions are disallowed from being used for security reasons.', 'pods' ), + 'customized' => __( 'Customized - Only allow a list of specific PHP function callbacks.', 'pods' ), + '0' => __( 'Disable display callbacks', 'pods' ), + ], + 'site_health_data' => [ + 'restricted' => __( 'Restricted', 'pods' ), + 'customized' => __( 'Customized', 'pods' ), + '0' => __( 'Disable', 'pods' ), + ], + 'depends-on' => [ + 'dynamic_features_allow' => '1', + ], + 'depends-on-multi' => [ + 'dynamic_features_enabled' => 'display', + ], + 'site_health_include_in_info' => true, + ]; + + $fields['display_callbacks_allowed'] = [ + 'name' => 'display_callbacks_allowed', + 'label' => __( 'Display callbacks allowed', 'pods' ), + 'description' => __( 'Please provide a comma-separated list of PHP function names to allow in callbacks.', 'pods' ), + 'type' => 'text', + 'default' => 'esc_attr,esc_html', + 'depends-on' => [ + 'dynamic_features_allow' => '1', + 'display_callbacks' => 'allowed', + ], + 'depends-on-multi' => [ + 'dynamic_features_enabled' => 'display', + ], + 'site_health_include_in_info' => true, + ]; + + return $fields; +} + +/** + * Get the bleep placeholder text. + * + * @since 3.1.0 + * + * @return string The bleep placeholder text. + */ +function pods_access_bleep_placeholder(): string { + return '****************'; +} + +/** + * Process the value and bleep it if it needs to be. + * + * @since 3.1.0 + * + * @param string|mixed $value The value to be bleeped. + * + * @return string|mixed The bleeped text if not empty, otherwise the value as it was. + */ +function pods_access_bleep_text( $value ) { + $bleep_text = pods_access_bleep_placeholder(); + + if ( 0 < strlen( (string) $value ) ) { + $value = $bleep_text; + } + + return $value; +} + +/** + * Process the data and bleep anything that needs to be. + * + * @since 3.1.0 + * + * @param array|object $data The data to be bleeped. + * @param array $additional_bleep_properties The additional properties to be bleeped from objects and arrays. + * + * @return array|object The bleeped data. + */ +function pods_access_bleep_data( $data, array $additional_bleep_properties = [] ) { + $bleep_properties = [ + 'user_pass', + 'user_activation_key', + 'post_password', + ]; + + /** + * Allow filtering the additional properties to be bleeped from objects and arrays. + * + * @since 3.1.0 + * + * @param array $additional_bleep_properties The additional properties to be bleeped from objects and arrays. + * @param array|object $data The data to be bleeped. + */ + $additional_bleep_properties = apply_filters( 'pods_access_bleep_properties', $additional_bleep_properties, $data ); + + $bleep_properties = array_merge( $bleep_properties, $additional_bleep_properties ); + + $bleep_text = pods_access_bleep_placeholder(); + + if ( is_object( $data ) ) { + foreach ( $bleep_properties as $bleep_property ) { + if ( isset( $data->{$bleep_property} ) ) { + $data->{$bleep_property} = 0 < strlen( (string) $data->{$bleep_property} ) ? $bleep_text : ''; + } + } + } elseif ( is_array( $data ) ) { + foreach ( $bleep_properties as $bleep_property ) { + if ( isset( $data[ $bleep_property ] ) ) { + $data[ $bleep_property ] = 0 < strlen( (string) $data[ $bleep_property ] ) ? $bleep_text : ''; + } + } + } + + return $data; +} + +/** + * Process the data and bleep anything that needs to be. + * + * @since 3.1.0 + * + * @param array $items The items to be bleeped. + * @param array $additional_bleep_properties The additional properties to be bleeped from objects and arrays. + * + * @return array|object The bleeped data. + */ +function pods_access_bleep_items( array $items, array $additional_bleep_properties = [] ) { + // Call the pods_access_bleep_data() function for all items in the $items array. + return array_map( + static function ( $item ) use ( $additional_bleep_properties ) { + return pods_access_bleep_data( $item, $additional_bleep_properties ); + }, + $items + ); +} + +/** + * Determine whether the SQL fragment is allowed to be used. + * + * @since 3.1.0 + * + * @param string $sql The SQL fragment to check. + * @param string $context The SQL fragment context. + * @param array $args { + * The arguments to use. + * + * @type string|null $object_type The object type. + * @type string|null $object_name The object name. + * @type int|string|null $item_id The item ID. + * @type Pods|null $pods The Pods object. + * @type Pod|null $pod The Pod object. + * @type bool $build_pods Whether to try to build a Pods object from the object type/name/ID (false by default). + * @type bool $build_pod Whether to try to build a Pod object from the object type/name (false by default). + * } + * + * @return bool Whether the SQL fragment is allowed to be used. + */ +function pods_access_sql_fragment_is_allowed( string $sql, string $context, array $args = [] ): bool { + $context = strtoupper( $context ); + + $info = pods_info_from_args( $args ); + + /** + * Allows filtering whether the SQL fragment is allowed to be used. + * + * @since 3.1.0 + * + * @param bool $allowed Whether the SQL fragment is allowed to be used. + * @param string $sql The SQL fragment to check. + * @param string $context The SQL fragment context. + * @param array $info Pod information. + */ + return (bool) apply_filters( 'pods_access_sql_fragment_is_allowed', true, $sql, $context, $info ); +} + +add_filter( 'pods_access_sql_fragment_is_allowed', 'pods_access_sql_fragment_disallow_mismatch_parenthesis', 10, 2 ); +add_filter( 'pods_access_sql_fragment_is_allowed', 'pods_access_sql_fragment_disallow_unsafe_functions', 10, 2 ); +add_filter( 'pods_access_sql_fragment_is_allowed', 'pods_access_sql_fragment_disallow_unsafe_tables', 10, 2 ); +add_filter( 'pods_access_sql_fragment_is_allowed', 'pods_access_sql_fragment_disallow_double_hyphens', 10, 2 ); +add_filter( 'pods_access_sql_fragment_is_allowed', 'pods_access_sql_fragment_disallow_subqueries', 10, 2 ); +add_filter( 'pods_access_sql_fragment_is_allowed', 'pods_access_sql_fragment_disallow_post_status', 10, 4 ); + +/** + * Disallow mismatched parenthesis from being used in SQL fragments. + * + * @since 3.1.0 + * + * @param bool $allowed Whether the SQL fragment is allowed to be used. + * @param string $sql The SQL fragment to check. + * + * @return bool Whether the SQL fragment is allowed to be used. + */ +function pods_access_sql_fragment_disallow_mismatch_parenthesis( bool $allowed, string $sql ): bool { + return ( + $allowed + && substr_count( $sql, '(' ) === substr_count( $sql, ')' ) + ); +} + +/** + * Disallow unsafe functions from being used in SQL fragments. + * + * @since 3.1.0 + * + * @param bool $allowed Whether the SQL fragment is allowed to be used. + * @param string $sql The SQL fragment to check. + * + * @return bool Whether the SQL fragment is allowed to be used. + */ +function pods_access_sql_fragment_disallow_unsafe_functions( bool $allowed, string $sql ): bool { + if ( ! $allowed ) { + return $allowed; + } + + $unsafe_functions = [ + 'USER', + 'DATABASE', + 'VERSION', + 'FROM_BASE64', + 'TO_BASE64', + 'SLEEP', + 'WAIT_FOR_EXECUTED_GTID_SET', + 'WAIT_UNTIL_SQL_THREAD_AFTER_GTIDS', + 'MASTER_POS_WAIT', + 'SOURCE_POS_WAIT', + 'LOAD_FILE', + ]; + + /** + * Allow filtering the list of unsafe functions to disallow. + * + * @since 3.1.0 + * + * @param array $unsafe_functions The list of unsafe functions to disallow. + * @param string $sql The SQL fragment to check. + */ + $unsafe_functions = (array) apply_filters( 'pods_access_sql_fragment_disallow_unsafe_functions', $unsafe_functions, $sql ); + + $unsafe_functions = array_filter( $unsafe_functions ); + + foreach ( $unsafe_functions as $unsafe_function ) { + if ( 1 === (int) preg_match( '/\s*' . preg_quote( $unsafe_function, '/' ) . '\s*\(/i', $sql ) ) { + return false; + } + } + + return $allowed; +} + +/** + * Disallow unsafe tables from being used in SQL fragments. + * + * @since 3.1.0 + * + * @param bool $allowed Whether the SQL fragment is allowed to be used. + * @param string $sql The SQL fragment to check. + * + * @return bool Whether the SQL fragment is allowed to be used. + */ +function pods_access_sql_fragment_disallow_unsafe_tables( bool $allowed, string $sql ): bool { + if ( ! $allowed ) { + return $allowed; + } + + $unsafe_tables = [ + 'mysql.', + 'information_schema.', + 'performance_schema.', + 'sys.', + ]; + + /** + * Allow filtering the list of unsafe tables to disallow. + * + * @since 3.1.0 + * + * @param array $unsafe_tables The list of unsafe tables to disallow. + * @param string $sql The SQL fragment to check. + */ + $unsafe_tables = (array) apply_filters( 'pods_access_sql_fragment_disallow_unsafe_tables', $unsafe_tables, $sql ); + + $unsafe_tables = array_filter( $unsafe_tables ); + + foreach ( $unsafe_tables as $unsafe_table ) { + if ( 1 === (int) preg_match( '/\s*' . preg_quote( $unsafe_table, '/' ) . '/i', $sql ) ) { + return false; + } + } + + return $allowed; +} + +/** + * Disallow double hyphens from being used in SQL fragments. + * + * @since 3.1.0 + * + * @param bool $allowed Whether the SQL fragment is allowed to be used. + * @param string $sql The SQL fragment to check. + * + * @return bool Whether the SQL fragment is allowed to be used. + */ +function pods_access_sql_fragment_disallow_double_hyphens( bool $allowed, string $sql ): bool { + return ( + $allowed + && false === strpos( $sql, '--' ) + ); +} + +/** + * Disallow subqueries from being used in SQL fragments. + * + * @since 3.1.0 + * + * @param bool $allowed Whether the SQL fragment is allowed to be used. + * @param string $sql The SQL fragment to check. + * + * @return bool Whether the SQL fragment is allowed to be used. + */ +function pods_access_sql_fragment_disallow_subqueries( bool $allowed, string $sql ): bool { + return ( + $allowed + && 0 === (int) preg_match( '/\s*SELECT(\s|\()+/i', $sql ) + ); +} + +/** + * Disallow post_status from being used in the WHERE/HAVING SQL fragment unless they have admin access. + * + * @since 3.1.0 + * + * @param bool $allowed Whether the SQL fragment is allowed to be used. + * @param string $sql The SQL fragment to check. + * @param string $context The SQL fragment context. + * @param array $info Pod information. + * + * @return bool Whether the SQL fragment is allowed to be used. + */ +function pods_access_sql_fragment_disallow_post_status( bool $allowed, string $sql, string $context, array $info ): bool { + if ( 'WHERE' !== $context && 'HAVING' !== $context ) { + return $allowed; + } + + return ( + $allowed + && ( + false === stripos( $sql, 'post_status' ) + || pods_is_admin( 'edit_posts' ) + ) + ); +} + +/** + * Safely unserialize data if it's PHP serialized. + * + * @since 3.1.0 + * + * @param string|mixed $data The data to unserialize. + * + * @return array|string|mixed The unserialized data if it was PHP serialized, otherwise the data as it was. + */ +function pods_maybe_safely_unserialize( $data ) { + // The $options parameter of unserialize() requires PHP 7.0+. + if ( version_compare( PHP_VERSION, '7.0', '<' ) ) { + // Fall back to normal WP function. + return maybe_unserialize( $data ); + } + + // Check if the data is serialized. + if ( is_serialized( $data ) ) { + $data = trim( $data ); + + // Unserialize the data but exclude classes. + return @unserialize( $data, [ 'allowed_classes' => false ] ); + } + + return $data; +} diff --git a/includes/classes.php b/includes/classes.php index 17566f61e5..f75e68b02c 100644 --- a/includes/classes.php +++ b/includes/classes.php @@ -51,7 +51,7 @@ function pods( $type = null, $id = null, $strict = null ) { * @param bool $strict (optional) If set to true, returns false instead of a Pods object, if the Pod itself doesn't * exist. Note: If you want to check if the Pods Item itself doesn't exist, use exists(). * - * @return bool|\Pods returns false if $strict, WP_DEBUG, PODS_STRICT or (PODS_DEPRECATED && PODS_STRICT_MODE) are true + * @return bool|\Pods|WP_Error returns false if $strict, WP_DEBUG, PODS_STRICT or (PODS_DEPRECATED && PODS_STRICT_MODE) are true * * @link https://docs.pods.io/code/pods/ */ @@ -259,14 +259,15 @@ function pods_i18n() { * @param string $cache_mode (optional) Specify the caching method to use for the view, available options include * cache, transient, or site-transient * @param bool $return (optional) Whether to return the view or not, defaults to false and will echo it + * @param bool $limited (optional) Whether to limit the view to only the theme directory, defaults to false * * @return string|bool The view output * * @since 2.0.0 * @link https://docs.pods.io/code/pods-view/ */ -function pods_view( $view, $data = null, $expires = false, $cache_mode = 'cache', $return = false ) { - $view = PodsView::view( $view, $data, $expires, $cache_mode ); +function pods_view( $view, $data = null, $expires = false, $cache_mode = 'cache', $return = false, $limited = false ) { + $view = PodsView::view( $view, $data, $expires, $cache_mode, $limited ); if ( $return ) { return $view; diff --git a/includes/data.php b/includes/data.php index f5381d7dd2..d6b1212b2b 100644 --- a/includes/data.php +++ b/includes/data.php @@ -660,6 +660,8 @@ function pods_v( $var = null, $type = 'get', $default = null, $strict = false, $ case 'globals': if ( isset( $GLOBALS[ $var ] ) ) { $output = $GLOBALS[ $var ]; + + $output = pods_access_bleep_data( $output ); } break; case 'cookie': @@ -681,9 +683,9 @@ function pods_v( $var = null, $type = 'get', $default = null, $strict = false, $ if ( is_user_logged_in() ) { $user = get_userdata( get_current_user_id() ); - if ( 'user_pass' === $var || 'user_activation_key' === $var ) { - $value = ''; - } elseif ( isset( $user->{$var} ) ) { + $user = pods_access_bleep_data( $user ); + + if ( isset( $user->{$var} ) ) { $value = $user->{$var}; } elseif ( 'role' === $var ) { $value = ''; @@ -1295,18 +1297,18 @@ function pods_cast( $value, $cast_from = null ) { } /** - * Create a slug from an input string + * Create a sanitized slug from a value. * - * @param string $orig Original string. - * @param bool $strict Whether to only support 0-9, a-z, A-Z, and dash characters. + * @since 1.8.9 * - * @return string Sanitized slug + * @param string $value The value to create the slug from. + * @param bool $strict Whether to only support 0-9, a-z, A-Z, and dash characters. * - * @since 1.8.9 + * @return string The sanitized slug. */ -function pods_create_slug( $orig, $strict = true ) { - $str = remove_accents( $orig ); - $str = preg_replace( '/([_ \\/])/', '-', trim( $orig ) ); +function pods_create_slug( $value, $strict = true ) { + $str = remove_accents( $value ); + $str = preg_replace( '/([_ \\/])/', '-', trim( $str ) ); if ( $strict ) { $str = preg_replace( '/([^0-9a-z\-])/', '', strtolower( $str ) ); @@ -1316,9 +1318,17 @@ function pods_create_slug( $orig, $strict = true ) { $str = preg_replace( '/(\-){2,}/', '-', $str ); $str = trim( $str, '-' ); - $str = apply_filters( 'pods_create_slug', $str, $orig ); - return $str; + /** + * Allow filtering the sanitized slug. + * + * @since 1.8.9 + * + * @param string $str The sanitized slug. + * @param string $value The value to create the slug from. + * @param bool $strict Whether to only support 0-9, a-z, A-Z, and dash characters. + */ + return (string) apply_filters( 'pods_create_slug', $str, $value ); } /** @@ -2010,15 +2020,7 @@ function pods_serial_comma( $value, $field = null, $fields = null, $and = null, return $value; } - // If something happens with table info, and this is a single select relationship, avoid letting user pass through. - if ( isset( $value['user_pass'] ) ) { - unset( $value['user_pass'] ); - - // Since we know this is a single select, just pass display name through as the fallback. - if ( isset( $value['display_name'] ) ) { - $value = array( $value['display_name'] ); - } - } + $value = pods_access_bleep_data( $value ); $original_value = $value; diff --git a/includes/general.php b/includes/general.php index c7ee4c4af5..3fbdf0a1cf 100644 --- a/includes/general.php +++ b/includes/general.php @@ -653,6 +653,11 @@ function pods_is_admin( $capabilities = null ) { * @return bool Whether user has admin access. */ function pods_is_user_admin( int $user_id, $capabilities = null ): bool { + // Invalid user. + if ( $user_id < 1 ) { + return false; + } + // Normalize the capability we are checking. if ( empty( $capabilities ) ) { $capabilities = []; @@ -1326,6 +1331,22 @@ function pods_doing_json() { * @return string */ function pods_shortcode( $tags, $content = null ) { + return pods_shortcode_run_safely( $tags, $content ); +} + +/** + * Shortcode support for use anywhere that support WP Shortcodes. + * Will return error message on failure. + * + * @since 3.1.0 + * + * @param array $tags An associative array of shortcode properties. + * @param string|null $content A string that represents a template override. + * @param bool $check_display_access_rights Whether to check access rights for the embedded content. + * + * @return string + */ +function pods_shortcode_run_safely( array $tags, ?string $content = null, bool $check_display_access_rights = true ): string { pods_doing_shortcode( true ); $return_exception = static function() { @@ -1335,8 +1356,18 @@ function pods_shortcode( $tags, $content = null ) { add_filter( 'pods_error_mode', $return_exception, 50 ); add_filter( 'pods_error_exception_fallback_enabled', '__return_false', 50 ); + $blog_is_switched = false; + + if ( defined( 'PODS_SHORTCODE_ALLOW_BLOG_SWITCHING' ) && PODS_SHORTCODE_ALLOW_BLOG_SWITCHING && is_multisite() ) { + if ( ! empty( $tags['blog_id'] ) && is_numeric( $tags['blog_id'] ) && (int) get_current_blog_id() !== (int) $tags['blog_id'] ) { + switch_to_blog( (int) $tags['blog_id'] ); + + $blog_is_switched = true; + } + } + try { - $return = pods_shortcode_run( $tags, $content ); + $return = pods_shortcode_run( $tags, $content, $blog_is_switched, $check_display_access_rights ); } catch ( Throwable $throwable ) { /** * Allow filtering whether to throw errors for the shortcode. @@ -1348,6 +1379,10 @@ function pods_shortcode( $tags, $content = null ) { $throw_errors = apply_filters( 'pods_shortcode_throw_errors', false ); if ( $throw_errors ) { + if ( $blog_is_switched ) { + restore_current_blog(); + } + throw $throwable; } @@ -1379,7 +1414,7 @@ function pods_shortcode( $tags, $content = null ) { sprintf( '%1$s: %2$s', esc_html__( 'Pods Renderer Error', 'pods' ), - esc_html__( 'There was a problem displaying this content, enable WP_DEBUG in wp-config.php to show more details.', 'pods' ) + esc_html__( 'There was a problem displaying this content, enable WP_DEBUG in wp-config.php to show more details.', 'pods' ) ), 'error', true @@ -1387,6 +1422,10 @@ function pods_shortcode( $tags, $content = null ) { } } + if ( $blog_is_switched ) { + restore_current_blog(); + } + remove_filter( 'pods_error_mode', $return_exception, 50 ); remove_filter( 'pods_error_exception_fallback_enabled', '__return_false', 50 ); @@ -1449,13 +1488,21 @@ function pods_wrap_html( $html, $attributes = [] ) { * * @since 2.7.13 * - * @param array $tags An associative array of shortcode properties. - * @param string $content A string that represents a template override. + * @param array $tags An associative array of shortcode properties. + * @param string|null $content A string that represents a template override. + * @param bool $blog_is_switched Whether the blog is switched. + * @param bool $check_display_access_rights Whether to check access rights for the embedded content. * * @return string */ -function pods_shortcode_run( $tags, $content = null ) { +function pods_shortcode_run( $tags, $content = null, $blog_is_switched = false, $check_display_access_rights = true ) { if ( defined( 'PODS_DISABLE_SHORTCODE' ) && PODS_DISABLE_SHORTCODE ) { + if ( empty( $tags['field'] ) && pods_is_admin() ) { + return pods_get_access_admin_notice( [ + 'content' => esc_html__( 'Pods dynamic features are disabled.', 'pods' ), + ] ); + } + return ''; } @@ -1509,6 +1556,7 @@ function pods_shortcode_run( $tags, $content = null ) { 'helper' => null, 'form' => null, 'form_output_type' => 'div', + 'form_key' => null, 'fields' => null, 'label' => null, 'thank_you' => null, @@ -1524,13 +1572,28 @@ function pods_shortcode_run( $tags, $content = null ) { $defaults = array_merge( $default_other_tags, $default_query_tags ); + $original_tags = $tags; + if ( ! empty( $tags ) ) { $tags = array_merge( $defaults, $tags ); } else { $tags = $defaults; } - $tags = apply_filters( 'pods_shortcode', $tags ); + // Bypass custom select if it might be aliasing or selecting data we don't want to work with. + if ( + ! empty( $tags['select'] ) + && is_string( $tags['select'] ) + && ( + false !== stripos( $tags['select'], 'user_pass' ) + || false !== stripos( $tags['select'], 'user_activation_key' ) + || false !== stripos( $tags['select'], 'post_password' ) + ) + ) { + $tags['select'] = null; + } + + $tags = apply_filters( 'pods_shortcode', $tags, $content, $original_tags ); $tags['pagination'] = filter_var( $tags['pagination'], FILTER_VALIDATE_BOOLEAN ); $tags['search'] = filter_var( $tags['search'], FILTER_VALIDATE_BOOLEAN ); @@ -1544,8 +1607,12 @@ function pods_shortcode_run( $tags, $content = null ) { if ( $tags['view'] && 0 < strlen( (string) $tags['view'] ) ) { $return = ''; - if ( ( ! defined( 'PODS_SHORTCODE_ALLOW_VIEWS' ) || PODS_SHORTCODE_ALLOW_VIEWS ) && ! file_exists( $tags['view'] ) ) { - $return = pods_view( $tags['view'], null, (int) $tags['expires'], $tags['cache_mode'], true ); + // Confirm the feature is enabled and the file is allowed. + if ( + pods_can_use_dynamic_feature( 'view' ) + && PodsView::view_get_path( $tags['view'], true ) + ) { + $return = pods_view( $tags['view'], null, (int) $tags['expires'], $tags['cache_mode'], true, true ); if ( $tags['shortcodes'] && defined( 'PODS_SHORTCODE_ALLOW_SUB_SHORTCODES' ) && PODS_SHORTCODE_ALLOW_SUB_SHORTCODES ) { $return = do_shortcode( $return ); @@ -1567,14 +1634,25 @@ function pods_shortcode_run( $tags, $content = null ) { return apply_filters( 'pods_shortcode_output', $return, $tags, null, 'view' ); } - $blog_is_switched = false; + $is_form = ! empty( $tags['form'] ); - if ( defined( 'PODS_SHORTCODE_ALLOW_BLOG_SWITCHING' ) && PODS_SHORTCODE_ALLOW_BLOG_SWITCHING && is_multisite() ) { - if ( ! empty( $tags['blog_id'] ) && is_numeric( $tags['blog_id'] ) && (int) get_current_blog_id() !== (int) $tags['blog_id'] ) { - switch_to_blog( (int) $tags['blog_id'] ); + // If the feature is disabled then return early. + if ( $is_form && ! pods_can_use_dynamic_feature( 'form' ) ) { + if ( pods_is_admin() ) { + return pods_get_access_admin_notice( [ + 'content' => esc_html__( 'The Pods Form dynamic feature is disabled and this embed will not show.', 'pods' ), + ] ); + } - $blog_is_switched = true; + return ''; + } elseif ( ! $is_form && ! pods_can_use_dynamic_feature( 'display' ) ) { + if ( empty( $tags['field'] ) && pods_is_admin() ) { + return pods_get_access_admin_notice( [ + 'content' => esc_html__( 'The Pods Display dynamic feature is disabled and this embed will not show.', 'pods' ), + ] ); } + + return ''; } if ( ! $tags['use_current'] && empty( $tags['name'] ) ) { @@ -1607,11 +1685,15 @@ function pods_shortcode_run( $tags, $content = null ) { } if ( ! $tags['use_current'] && empty( $tags['name'] ) ) { - if ( $blog_is_switched ) { - restore_current_blog(); - } - - return '

    ' . esc_html__( 'Pods embed error: Please provide a Pod name', 'pods' ) . '

    '; + return pods_message( + sprintf( + '%1$s: %2$s', + esc_html__( 'Pods Embed Error', 'pods' ), + esc_html__( 'Please provide a Pod name.', 'pods' ) + ), + 'error', + true + ); } } @@ -1628,11 +1710,15 @@ function pods_shortcode_run( $tags, $content = null ) { } if ( empty( $content ) && empty( $tags['pods_page'] ) && empty( $tags['template'] ) && empty( $tags['field'] ) && empty( $tags['form'] ) ) { - if ( $blog_is_switched ) { - restore_current_blog(); - } - - return '

    ' . esc_html__( 'Pods embed error: Please provide either a template or field name', 'pods' ) . '

    '; + return pods_message( + sprintf( + '%1$s: %2$s', + esc_html__( 'Pods Embed Error', 'pods' ), + esc_html__( 'Please provide either a template or field name.', 'pods' ) + ), + 'error', + true + ); } if ( ! $tags['use_current'] && ! isset( $id ) ) { @@ -1676,11 +1762,15 @@ function pods_shortcode_run( $tags, $content = null ) { } if ( empty( $pod ) || ! $pod->valid() ) { - if ( $blog_is_switched ) { - restore_current_blog(); - } - - return '

    ' . esc_html__( 'Pods embed error: Pod not found', 'pods' ) . '

    '; + return pods_message( + sprintf( + '%1$s: %2$s', + esc_html__( 'Pods Embed Error', 'pods' ), + esc_html__( 'Pod not found.', 'pods' ) + ), + 'error', + true + ); } $found = 0; @@ -1688,47 +1778,181 @@ function pods_shortcode_run( $tags, $content = null ) { $is_singular = ( ! empty( $id ) || $tags['use_current'] ); + $return = ''; + + $info = pods_info_from_args( [ + 'item_id' => $is_singular ? $id : null, + 'pods' => $pod, + ] ); + + // Determine if this is is a public content type. + if ( + $check_display_access_rights + ) { + // Check access rights for editor mode and preview of this embed. + if ( ! empty( $tags['_is_editor_mode'] ) || ! empty( $tags['_is_preview'] ) || is_preview() ) { + $check_post_id = ! empty( $tags['_preview_id'] ) ? (int) $tags['_preview_id'] : get_queried_object_id(); + + if ( ! current_user_can( 'publish_post', $check_post_id ) && ! pods_is_admin() ) { + // Stop display and only return the notice. + return empty( $tags['field'] ) ? pods_get_access_user_notice( $info, true, esc_html__( 'You do not have the capability to preview this Pods embed.', 'pods' ) ) : ''; + } + } + + $access_type = 'read'; + + if ( $is_form ) { + $access_type = $is_singular ? 'edit' : 'add'; + } + + if ( + ! pods_is_type_public( $info ) + || ! pods_can_use_dynamic_feature_unrestricted( $info, $is_form ? 'form' : 'display', $access_type ) + ) { + + // Stop handling the display and return the access notice if they do not have access to the private content type. + if ( ! pods_current_user_can_access_object( $info, $access_type, 'shortcode' ) ) { + // Stop display and only return the notice. + return empty( $tags['field'] ) ? pods_get_access_user_notice( $info ) : ''; + } + + // Show the admin-specific notice that this content may not be visible to others since it is not public. + if ( empty( $tags['field'] ) && pods_is_admin() ) { + // Include the notice in the display output to let the admin know and continue the display. + $return .= pods_get_access_admin_notice( $info ); + } + } elseif ( + $check_display_access_rights + && ( + pods_access_bypass_post_with_password( $info ) + || pods_access_bypass_private_post( $info ) + ) + ) { + // Stop display and only return the notice. + return empty( $tags['field'] ) ? pods_get_access_user_notice( $info ) : ''; + } + } + if ( ! $is_singular ) { $params = array(); - if ( ! defined( 'PODS_DISABLE_SHORTCODE_SQL' ) || ! PODS_DISABLE_SHORTCODE_SQL ) { - $evaluate_tags_args = array( - 'sanitize' => true, - 'fallback' => '""', - 'use_current_pod' => true, - ); + $can_use_dynamic_feature_all_sql_clauses = pods_can_use_dynamic_feature_sql_clauses( 'all' ); + $can_use_dynamic_feature_simple_sql_clauses = pods_can_use_dynamic_feature_sql_clauses( 'simple' ); + $shortcode_allow_evaluate_tags = pods_shortcode_allow_evaluate_tags(); - if ( $tags['orderby'] && 0 < strlen( (string) $tags['orderby'] ) ) { - $params['orderby'] = $tags['orderby']; + $evaluate_tags_args = array( + 'sanitize' => true, + 'fallback' => '""', + 'use_current_pod' => true, + ); + + if ( $tags['select'] && 0 < strlen( (string) $tags['select'] ) ) { + if ( ! $can_use_dynamic_feature_simple_sql_clauses || ! pods_access_sql_fragment_is_allowed( $tags['select'], 'SELECT', $info ) ) { + return pods_message( + sprintf( + '%1$s: %2$s', + esc_html__( 'Pods Embed Error', 'pods' ), + esc_html__( 'SELECT contains SQL that is not allowed.', 'pods' ) + ), + 'error', + true + ); } - if ( $tags['where'] && 0 < strlen( (string) $tags['where'] ) ) { - $params['where'] = $tags['where']; + $params['select'] = $tags['select']; + } - if ( pods_shortcode_allow_evaluate_tags() ) { - $params['where'] = pods_evaluate_tags_sql( html_entity_decode( $params['where'] ), $evaluate_tags_args ); - } + if ( $tags['join'] && 0 < strlen( (string) $tags['join'] ) ) { + if ( ! $can_use_dynamic_feature_all_sql_clauses || ! pods_access_sql_fragment_is_allowed( $tags['join'], 'JOIN', $info ) ) { + return pods_message( + sprintf( + '%1$s: %2$s', + esc_html__( 'Pods Embed Error', 'pods' ), + esc_html__( 'JOIN contains SQL that is not allowed.', 'pods' ) + ), + 'error', + true + ); } - if ( $tags['having'] && 0 < strlen( (string) $tags['having'] ) ) { - $params['having'] = $tags['having']; + $params['join'] = $tags['join']; + } - if ( pods_shortcode_allow_evaluate_tags() ) { - $params['having'] = pods_evaluate_tags_sql( html_entity_decode( $params['having'] ), $evaluate_tags_args ); - } + if ( $tags['where'] && 0 < strlen( (string) $tags['where'] ) ) { + $tags['where'] = ltrim( $tags['where'], ')' ); + + if ( ! $can_use_dynamic_feature_simple_sql_clauses || ! pods_access_sql_fragment_is_allowed( $tags['where'], 'WHERE', $info ) ) { + return pods_message( + sprintf( + '%1$s: %2$s', + esc_html__( 'Pods Embed Error', 'pods' ), + esc_html__( 'WHERE contains SQL that is not allowed.', 'pods' ) + ), + 'error', + true + ); } - if ( $tags['groupby'] && 0 < strlen( (string) $tags['groupby'] ) ) { - $params['groupby'] = $tags['groupby']; + $params['where'] = $tags['where']; + + if ( $shortcode_allow_evaluate_tags ) { + $params['where'] = pods_evaluate_tags_sql( html_entity_decode( $params['where'] ), $evaluate_tags_args ); } + } - if ( $tags['select'] && 0 < strlen( (string) $tags['select'] ) ) { - $params['select'] = $tags['select']; + if ( $tags['groupby'] && 0 < strlen( (string) $tags['groupby'] ) ) { + if ( ! $can_use_dynamic_feature_all_sql_clauses || ! pods_access_sql_fragment_is_allowed( $tags['groupby'], 'GROUP BY', $info ) ) { + return pods_message( + sprintf( + '%1$s: %2$s', + esc_html__( 'Pods Embed Error', 'pods' ), + esc_html__( 'GROUP BY contains SQL that is not allowed.', 'pods' ) + ), + 'error', + true + ); } - if ( $tags['join'] && 0 < strlen( (string) $tags['join'] ) ) { - $params['join'] = $tags['join']; + + $params['groupby'] = $tags['groupby']; + } + + if ( $tags['having'] && 0 < strlen( (string) $tags['having'] ) ) { + $tags['having'] = ltrim( $tags['having'], ')' ); + + if ( ! $can_use_dynamic_feature_all_sql_clauses || ! pods_access_sql_fragment_is_allowed( $tags['having'], 'HAVING', $info ) ) { + return pods_message( + sprintf( + '%1$s: %2$s', + esc_html__( 'Pods Embed Error', 'pods' ), + esc_html__( 'HAVING contains SQL that is not allowed.', 'pods' ) + ), + 'error', + true + ); } - }//end if + + $params['having'] = $tags['having']; + + if ( $shortcode_allow_evaluate_tags ) { + $params['having'] = pods_evaluate_tags_sql( html_entity_decode( $params['having'] ), $evaluate_tags_args ); + } + } + + if ( $tags['orderby'] && 0 < strlen( (string) $tags['orderby'] ) ) { + if ( ! $can_use_dynamic_feature_simple_sql_clauses || ! pods_access_sql_fragment_is_allowed( $tags['orderby'], 'ORDER BY', $info ) ) { + return pods_message( + sprintf( + '%1$s: %2$s', + esc_html__( 'Pods Embed Error', 'pods' ), + esc_html__( 'ORDER BY contains SQL that is not allowed.', 'pods' ) + ), + 'error', + true + ); + } + + $params['orderby'] = $tags['orderby']; + } // Load filters and return HTML for later use. if ( @@ -1790,40 +2014,35 @@ function pods_shortcode_run( $tags, $content = null ) { }//end if // Handle form output. - if ( ! empty( $tags['form'] ) ) { + if ( $is_form ) { if ( 'user' === $pod->pod ) { - if ( false !== strpos( $tags['fields'], '_capabilities' ) || false !== strpos( $tags['fields'], '_user_level' ) ) { - if ( $blog_is_switched ) { - restore_current_blog(); - } - + if ( + false !== strpos( $tags['fields'], '_capabilities' ) + || false !== strpos( $tags['fields'], '_capabilities' ) + || false !== strpos( $tags['fields'], 'role' ) + ) { // Further hardening of User-based forms - return ''; + return pods_get_access_user_notice( $info, false, __( 'You cannot edit role or capabilities for users with Pods', 'pods' ) ); } elseif ( $is_singular && ( ! defined( 'PODS_SHORTCODE_ALLOW_USER_EDIT' ) || ! PODS_SHORTCODE_ALLOW_USER_EDIT ) ) { - if ( $blog_is_switched ) { - restore_current_blog(); - } - // Only explicitly allow user edit forms - return ''; + return pods_get_access_user_notice( $info, false, __( 'Edit user profile forms have been disabled on this site.', 'pods' ) ); } } $form_params = [ - 'fields' => $tags['fields'], - 'label' => $tags['label'], - 'thank_you' => $tags['thank_you'], - 'output_type' => ! empty( $tags['form_output_type'] ) ? $tags['form_output_type'] : 'div', + 'fields' => $tags['fields'], + 'label' => $tags['label'], + 'thank_you' => $tags['thank_you'], + 'output_type' => ! empty( $tags['form_output_type'] ) ? $tags['form_output_type'] : 'div', + 'form_key' => $tags['form_key'], + // We already checked the access so we can bypass this. + 'check_access' => false, ]; - $return = $pod->form( $form_params ); + $return .= $pod->form( $form_params ); $return = pods_wrap_html( $return, $tags ); - if ( $blog_is_switched ) { - restore_current_blog(); - } - /** * Allow customization of shortcode output based on shortcode attributes. * @@ -1840,17 +2059,32 @@ function pods_shortcode_run( $tags, $content = null ) { // Handle field output. if ( ! empty( $tags['field'] ) ) { if ( $tags['template'] || $content ) { - $return = ''; $related = $pod->field( $tags['field'], array( 'output' => 'find' ) ); if ( $related instanceof Pods && $related->valid() ) { // Content is null by default. - $return .= $related->template( $tags['template'], $content ); + $return_output = $related->template( $tags['template'], $content ); + + if ( null !== $return_output ) { + $return .= $return_output; + } } } elseif ( empty( $tags['helper'] ) ) { - $return = $pod->display( $tags['field'] ); + $return_output = $pod->display( $tags['field'] ); + + if ( null !== $return_output ) { + $return .= $return_output; + } else { + $return = ''; + } } else { - $return = $pod->helper( $tags['helper'], $pod->field( $tags['field'] ), $tags['field'] ); + $return_output = $pod->helper( $tags['helper'], $pod->field( $tags['field'] ), $tags['field'] ); + + if ( null !== $return_output ) { + $return .= $return_output; + } else { + $return = ''; + } } // @todo $blog_is_switched >> Switch back before running other shortcodes? @@ -1860,10 +2094,6 @@ function pods_shortcode_run( $tags, $content = null ) { $return = pods_wrap_html( $return, $tags ); - if ( $blog_is_switched ) { - restore_current_blog(); - } - /** * Allow customization of shortcode output based on shortcode attributes. * @@ -1882,14 +2112,18 @@ function pods_shortcode_run( $tags, $content = null ) { $pods_page = Pods_Pages::exists( $tags['pods_page'] ); if ( empty( $pods_page ) ) { - if ( $blog_is_switched ) { - restore_current_blog(); - } - - return '

    ' . esc_html__( 'Pods embed error: Pods Page not found.', 'pods' ) . '

    '; + return pods_message( + sprintf( + '%1$s: %2$s', + esc_html__( 'Pods Embed Error', 'pods' ), + esc_html__( 'Pods Page not found.', 'pods' ) + ), + 'error', + true + ); } - $return = Pods_Pages::content( true, $pods_page ); + $return .= Pods_Pages::content( true, $pods_page ); // @todo $blog_is_switched >> Switch back before running other shortcodes? if ( $tags['shortcodes'] && defined( 'PODS_SHORTCODE_ALLOW_SUB_SHORTCODES' ) && PODS_SHORTCODE_ALLOW_SUB_SHORTCODES ) { @@ -1898,10 +2132,6 @@ function pods_shortcode_run( $tags, $content = null ) { $return = pods_wrap_html( $return, $tags ); - if ( $blog_is_switched ) { - restore_current_blog(); - } - /** * Allow customization of shortcode output based on shortcode attributes. * @@ -1953,7 +2183,7 @@ function pods_shortcode_run( $tags, $content = null ) { echo $pagination; } - $content = $pod->template( $tags['template'], $content ); + $content = $pod->template( $tags['template'], $content, false, true ); if ( '' === trim( $content ) && ! empty( $tags['not_found'] ) ) { $content = $pod->do_magic_tags( $tags['not_found'] ); @@ -1972,7 +2202,7 @@ function pods_shortcode_run( $tags, $content = null ) { echo $filters; } - $return = ob_get_clean(); + $return .= ob_get_clean(); if ( $tags['shortcodes'] && defined( 'PODS_SHORTCODE_ALLOW_SUB_SHORTCODES' ) && PODS_SHORTCODE_ALLOW_SUB_SHORTCODES ) { $return = do_shortcode( $return ); @@ -1980,10 +2210,6 @@ function pods_shortcode_run( $tags, $content = null ) { $return = pods_wrap_html( $return, $tags ); - if ( $blog_is_switched ) { - restore_current_blog(); - } - /** * Allow customization of shortcode output based on shortcode attributes. * @@ -3374,7 +3600,7 @@ function pods_meta_hook_list( $object_type = 'post', $object = null ) { $first_pods_version = get_option( 'pods_framework_version_first' ); $first_pods_version = '' === $first_pods_version ? PODS_VERSION : $first_pods_version; - $metadata_integration = ! $is_types_only && 1 === (int) pods_get_setting( 'metadata_integration', 1 ); + $metadata_integration = ! $is_types_only && 1 === (int) pods_get_setting( 'metadata_integration', ( function_exists( 'wc_get_product' ) || version_compare( $first_pods_version, '2.9.14', '<' ) ) ? '1' : '0' ); $watch_changed_fields = ! $is_types_only && 1 === (int) pods_get_setting( 'watch_changed_fields', version_compare( $first_pods_version, '2.8.21', '<=' ) ? 1 : 0 ); $media_modal_fields = ! $is_types_only && 1 === (int) pods_get_setting( 'media_modal_fields', version_compare( $first_pods_version, '2.9.16', '<=' ) ? 1 : 0 ); @@ -4051,10 +4277,19 @@ function pods_session_start() { */ function pods_session_id() { if ( false === pods_session_start() ) { - return ''; + $session_id = ''; + } else { + $session_id = @session_id(); } - return @session_id(); + /** + * Allow overriding the session ID used by Pods. + * + * @since 3.1.0 + * + * @param string $session_id The session ID. + */ + return (string) apply_filters( 'pods_session_id', $session_id ); } /** diff --git a/init.php b/init.php index ecb3c4f10d..8b884b2ea5 100644 --- a/init.php +++ b/init.php @@ -4,13 +4,13 @@ * * @package Pods * @author Pods Framework Team - * @copyright 2023 Pods Foundation, Inc + * @copyright 2024 Pods Foundation, Inc * @license GPL v2 or later * * Plugin Name: Pods - Custom Content Types and Fields * Plugin URI: https://pods.io/ * Description: Pods is a framework for creating, managing, and deploying customized content types and fields - * Version: 3.0.10 + * Version: 3.1.0 * Author: Pods Framework Team * Author URI: https://pods.io/about/ * Text Domain: pods @@ -43,7 +43,7 @@ add_action( 'init', 'pods_deactivate_pods_ui' ); } else { // Current version. - define( 'PODS_VERSION', '3.0.10' ); + define( 'PODS_VERSION', '3.1.0' ); // Current database version, this is the last version the database changed. define( 'PODS_DB_VERSION', '2.3.5' ); @@ -106,6 +106,7 @@ require_once PODS_DIR . 'vendor/vendor-prefixed/autoload.php'; // Include global functions. + require_once PODS_DIR . 'includes/access.php'; require_once PODS_DIR . 'includes/classes.php'; require_once PODS_DIR . 'includes/data.php'; require_once PODS_DIR . 'includes/forms.php'; diff --git a/package.json b/package.json index 8bf1ef8722..29917cb758 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pods", - "version": "3.0.10", + "version": "3.1.0", "description": "Pods is a development framework for creating, extending, managing, and deploying customized content types in WordPress.", "author": "Pods Foundation, Inc", "homepage": "https://pods.io/", diff --git a/readme.txt b/readme.txt index 267a3ebaaf..28edef1e73 100644 --- a/readme.txt +++ b/readme.txt @@ -3,9 +3,9 @@ Contributors: sc0ttkclark, zrothauser, keraweb, jimtrue, quasel, nicdford, james Donate link: https://friends.pods.io/ Tags: pods, custom post types, custom taxonomies, content types, custom fields, block Requires at least: 6.0 -Tested up to: 6.4 +Tested up to: 6.5 Requires PHP: 7.2 -Stable tag: 3.0.10 +Stable tag: 3.1.0 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -121,7 +121,7 @@ You can enable some of our included components to extend your WordPress site eve = Extend Pods with Free Third-party Add-Ons = * [Paid Memberships Pro - Pods Add On](https://wordpress.org/plugins/pmpro-pods/) - Integrates Pods with [Paid Memberships Pro](https://wordpress.org/plugins/paid-memberships-pro/) to extend PMPro objects with custom fields added by Pods -* [Panda Pods Repeater Field Add-On](https://wordpress.org/plugins/panda-pods-repeater-field/) - Lets you add groups of fields that repeat and are stored in their own custom database table (Advanced setup required) +* [Panda Pods Repeater Field Add-On](https://wordpress.org/plugins/panda-pods-repeater-field/) - Lets you add groups of fields that repeat and are stored in their own custom database table (Advanced setup required) = Pods Pro by SKCDEV Premium Add-Ons = @@ -181,17 +181,40 @@ Pods really wouldn't be where it is without all the contributions from our [dono == Changelog == += 3.1 - February 21st, 2024 = + +*Security Release* + +While this release is meant to be as backwards compatible as possible, some aspects of security hardening may require manual intervention by site owners and their developers. There were no known reports and no known attempts to take advantage of the issues resolved by this release except where noted. + +Read more about [How access rights work with Pods](https://docs.pods.io/displaying-pods/access-rights-in-pods/) for more details including new filters/snippets that can provide limited access. + +* Security hardening: Introduced new access checks and additional fine-grained control over dynamic features across any place in Pods that allows embedding content or forms. This only applies to usage through Pods Blocks or Shortcodes. Using PHP will continue to expect you are handling this on your own unless you pass the appropriate arguments to the corresponding Pods methods. (@sc0ttkclark) +* Security hardening: Prevent using the Pods Views Block / Shortcode to embed any files outside of the current theme. Props to the Nex Team / Wordfence for responsibly reporting this. (@sc0ttkclark) +* Security hardening: Prevent output of `user_pass`, `user_activation_key`, and `post_password` through Pods dynamic features / PHP. These values will be set in Pods references to `****************` if they were not-empty so you can still do conditional checks as normal. While Scott was already aware of this in pre-planned security release work, additional props go to the Nex Team / Wordfence for responsibly reporting this too. (@sc0ttkclark) +* Security hardening: Prevent more unsavory PHP display callbacks from being used with magic tags in addition to those already prevented. Props to the Nex Team / Wordfence for responsibly reporting this. (@sc0ttkclark) +* Feature: Access rights > Access-related Admin notices and Errors can be hidden by admins in a new setting in Pods Admin > Settings > Security. (@sc0ttkclark) +* Feature: Dynamic Features > Dynamic features (Pods Blocks and Shortcodes) can be disabled by admins in a new setting in Pods Admin > Settings > Security. (@sc0ttkclark) +* Changed: Dynamic Features > New installs will now default to not allowing all SQL arguments to be used by dynamic features. Existing installs will default to only allowing simple SQL arguments. All SQL fragments are checked for disallowed usage like subqueries. This can be set in a new setting in Pods Admin > Settings > Security. (@sc0ttkclark) +* Feature: Pods Display > The Display-related Pods Blocks and Shortcodes have additional checks that limit access to content based on the user viewing it. For Post Types that are non-public, they must have access to the `read` capability from that post type as a normal user. For displaying content from Users, they must have access to `list_users` capability to view that. [Read more about how access rights work with Pods](https://docs.pods.io/displaying-pods/access-rights-in-pods/) (@sc0ttkclark) +* Feature: Pods Forms > The Pods Form Block and Form Shortcode have additional checks that limit access to creating/editing content based on the user submitting the form. For Post Types that are non-public, they must have access to the 'create' capability from that post type as a normal user. Forms that submit to the Users pod, now require that the submitter must have access to the `create_users` or `edit_users` capability to create or edit that user. [Read more about how access rights work with Pods](https://docs.pods.io/displaying-pods/access-rights-in-pods/) (@sc0ttkclark) +* Feature: Pods Forms > The Pods Form Block and Form Shortcode now have a new option to identify the form with a custom key you choose that will get passed to various access-related filters so that developers can override access rights more easily. (@sc0ttkclark) +* Feature: Pods Forms > When a user has access to create or edit content through a Pods form for a post type, the `post_content` field is cleaned based on the level of access they have to prevent inserting unintentional shortcodes or blocks. (@sc0ttkclark) +* Feature: Markdown functionality has now been replaced by the [Parsedown library](https://github.com/erusev/parsedown) for better security and performance and it's uniquely prefixed so it prevents future conflicts with plugins using the same library. (@sc0ttkclark) +* Changed: Pods Views > One of the breaking changes in this work is that the Pods Views Block / Shortcode dynamic feature is now disabled by default and must be enabled for new and existing installs. This can be done in a new setting in Pods Admin > Settings > Security. (@sc0ttkclark) +* Changed: Display PHP callbacks > New installs will now default to only allowing specific callbacks to be used. This defaults the specific callbacks allowed to `esc_attr,esc_html` which can be further customized in Pods Admin > Settings > Security. (@sc0ttkclark) + = 3.0.10 - December 11th, 2023 = * Fixed: The safe rendering handler for Pods Blocks now properly passes along context to all Pods Blocks so that they work within Query Loops again and other places they could take on context. (@sc0ttkclark) * Fixed: Resolved PHP 8.3 deprecation notice with `get_class()` usage. #7225 (@netlas, @sc0ttkclark) -* Fixed: File fields using the direct plupload option will properly avoid uploading files above the limit and handle uploading multiple files without losing all but the first file in the file list. #7138 (@sc0ttkclark, @PD-CM) +* Fixed: File fields using the direct plupload option will properly avoid uploading files above the limit and handle uploading multiple files without losing all but the first file in the file list. #7138 (@sc0ttkclark, @PD-CM) = 3.0.9 - December 7th, 2023 = -* Feature: Added support for disabling Quick Edit for custom post types for sites on WP 6.4+. (@sc0ttkclark) +* Feature: Added support for disabling Quick Edit for custom post types for sites on WP 6.4+. (@sc0ttkclark) * Tweak: Revamped the Pods Blocks error handling so it looks nicer. (@sc0ttkclark) -* Tweak: Updated the default values used for "Add New" labels to use "Add New {singularLabel}" instead to follow WP core's direction. (@sc0ttkclark) +* Tweak: Updated the default values used for "Add New" labels to use "Add New {singularLabel}" instead to follow WP core's direction. (@sc0ttkclark) * Fixed: Custom capability types now save as empty and remain empty as intended. When empty, they will default to the current post type name and a placeholder shows what that would be. #7218 (@sc0ttkclark) * Fixed: Added more handling for SQL errors for Pods Blocks. #7207 (@sc0ttkclark) * Fixed: Resolved WP debug notices when calling `wp_cache_flush_group` by checking if `wp_wp_cache_supports( 'flush_group' )` first. (@sc0ttkclark) @@ -204,7 +227,7 @@ Pods really wouldn't be where it is without all the contributions from our [dono = 3.0.7 - October 19th, 2023 = * Fixed: Avoid duplicate fields showing up when registering fields manually through `pods_group_add()` instead of through normal DB or file-based configs. #6317 (@sc0ttkclark) -* Fixed: Resolve cases where Settings pod types would treat every save as a new item. (@sc0ttkclark) +* Fixed: Resolve cases where Settings pod types would treat every save as a new item. (@sc0ttkclark) * Fixed: Swept through the codebase to fix all remaining return type issues with PHP 8+ for the most common extended/implemented methods. (@sc0ttkclark) = 3.0.6 - October 2nd, 2023 = @@ -214,7 +237,7 @@ Pods really wouldn't be where it is without all the contributions from our [dono = 3.0.5 - October 2nd, 2023 = -* Tweak: Added the "full" image size to the reference list in the Pods Template editor. #7183 #7184 (@JoryHogeveen) +* Tweak: Added the "full" image size to the reference list in the Pods Template editor. #7183 #7184 (@JoryHogeveen) * Fixed: PHP deprecated notices resolved for WYSIWYG field. #7182 (@sc0ttkclark) * Fixed: Relationships with user capabilities filtered will now work as expected in more cases when using WP multisite. #7181 #7185 (@JoryHogeveen) * Fixed: Enforce single value for inputs instead of arrays of values in single format for file/relationship field. This resolves issues with Conditional Logic not seeing the initial value on reload. (@sc0ttkclark) @@ -238,7 +261,7 @@ Pods really wouldn't be where it is without all the contributions from our [dono * Fixed: Bi-directional field saving has been restored after breaking in Pods 2.9.x. #7172 (@sc0ttkclark) * Fixed: More PHP notices/warnings/errors with PHP 8+ addressed. #7170 #7171 (@sc0ttkclark) * Fixed: Settings saving for simple relationships fixed when using a setting pod registered by code/file. #7169 (@sc0ttkclark) -* Fixed: Settings forms would sometimes say the save failed when it was successful. (@sc0ttkclark) +* Fixed: Settings forms would sometimes say the save failed when it was successful. (@sc0ttkclark) * Fixed: Fix relationship rendering in tables for Advanced Content Types. #7173 (@sc0ttkclark) * Fixed: Potential SQL errors when a Pod does not exist but it's still set a relationship object for a field. (@sc0ttkclark) * Fixed: Pod config data is now correctly passed in all instances of `pods_meta_default_box_title` filter so it's uniform in usage. (@sc0ttkclark) @@ -265,7 +288,7 @@ New minimum required versions have been updated as follows which includes notice **3.0 Changelog** * Feature: [Conditional Logic for Fields](https://docs.pods.io/fields/conditional-logic-for-fields/) - You can now choose to show or hide a Pods field based on the value of another Pods field. More will be expanded into this functionality in the future. #609 (@zrothauser, @sc0ttkclark, @Shelob9) -* Feature: Support for the new Command Palettes feature added in WordPress 6.3. This will allow you to contextually go to the Edit Pod screen for the matching Pod configuration of that post type directly from the Edit Post screen. (@sc0ttkclark) +* Feature: Support for the new Command Palettes feature added in WordPress 6.3. This will allow you to contextually go to the Edit Pod screen for the matching Pod configuration of that post type directly from the Edit Post screen. (@sc0ttkclark) * Added: Set up backward compatible updates to our prior `depends-on` and related logic for internal conditional logic prior to Pods 3.0 so they now properly translate over to the new format required. (@sc0ttkclark) * Added: When importing and exporting Pods packages, they now include the list of active components. (@sc0ttkclark) * Fixed: Media saving issue has been resolved when the Media Modal fields are disabled, it now saves fields as expected. #7158 (@sc0ttkclark, @pd-cm) @@ -274,7 +297,7 @@ New minimum required versions have been updated as follows which includes notice * Fixed: When extending a post type or taxonomy it will now use the associated post type or taxonomy label when setting up the pod so it matches. #6350 (@sc0ttkclark) * Fixed: Resolved problems with saving the featured image from frontend forms when a post type supports featured images. (@sc0ttkclark) * Fixed: Don't show SQL errors when getting relationship data for forms if the debug display is disabled in WP. (@sc0ttkclark) -* Fixed: Improved tooltip appearances with better color contrast. #7119 #7118 (@heybran) +* Fixed: Improved tooltip appearances with better color contrast. #7119 #7118 (@heybran) * Removed: The Tribe Common library has been removed from Pods and it will alleviate conflicts from plugins like The Events Calendar and Event Tickets going forward. (@sc0ttkclark) = 2.9.19 - June 28th, 2023 = @@ -307,7 +330,7 @@ New minimum required versions have been updated as follows which includes notice = 2.9.15 - May 30th, 2023 = * Added: Now showing text notices to explain where performance may be improved for Relationship single/multi select fields when relating to dynamic content which direct the admin to use Autocomplete or List View instead in certain circumstances. (@sc0ttkclark) -* Added: Add repair tool that can help recover any Pods that have an invalid pod type / storage. (@sc0ttkclark) +* Added: Add repair tool that can help recover any Pods that have an invalid pod type / storage. (@sc0ttkclark) * Tweak: Make the default as Dropdown for single select relationship fields again after seeing some reported issues. (@sc0ttkclark) * Fixed: Resolved more PHP 8.x compatibility issues for `strlen()` usage. (@sc0ttkclark) * Fixed: Set default as Dropdown for relationship field configs across Pods. (@sc0ttkclark) diff --git a/sql/update-1.x.php b/sql/update-1.x.php index 33bf734cc9..7e406f845b 100644 --- a/sql/update-1.x.php +++ b/sql/update-1.x.php @@ -241,7 +241,7 @@ if ( version_compare( $old_version, '1.7.5', '<' ) ) { if ( empty( $pods_roles ) && ! is_array( $pods_roles ) ) { - $pods_roles = @unserialize( get_option( 'pods_roles' ) ); + $pods_roles = pods_maybe_safely_unserialize( get_option( 'pods_roles' ) ); if ( ! is_array( $pods_roles ) ) { $pods_roles = array(); diff --git a/sql/upgrade/PodsUpgrade_2_0_0.php b/sql/upgrade/PodsUpgrade_2_0_0.php index e663e5a875..c6b7c58576 100644 --- a/sql/upgrade/PodsUpgrade_2_0_0.php +++ b/sql/upgrade/PodsUpgrade_2_0_0.php @@ -740,7 +740,7 @@ public function migrate_roles() { $old_roles = get_option( 'pods_roles' ); if ( ! is_array( $old_roles ) && ! empty( $old_roles ) ) { - $old_roles = @unserialize( $old_roles ); + $old_roles = pods_maybe_safely_unserialize( $old_roles ); } if ( ! is_array( $old_roles ) ) { diff --git a/src/Pods/Admin/Config/Field.php b/src/Pods/Admin/Config/Field.php index a40e6fdbb5..c5155a571e 100644 --- a/src/Pods/Admin/Config/Field.php +++ b/src/Pods/Admin/Config/Field.php @@ -570,16 +570,27 @@ public function get_fields( \Pods\Whatsit\Pod $pod, array $tabs ) { } /** - * Modify Additional Field Options tab + * Modify Additional Field Options tab for a specific field type. * * @since 2.7.0 * - * @param array $type_options Additional field type options, - * @param string $type Field type, - * @param array $options Tabs, indexed by label, + * @param array $type_options Additional field type options. + * @param string $type Field type. + * @param array $options Tabs, indexed by label. * @param null|\Pods\Whatsit\Pod $pod Pods object for the Pod this UI is for. */ $type_options = apply_filters( "pods_admin_setup_edit_{$type}_additional_field_options", $type_options, $type, $options, $pod ); + + /** + * Modify Additional Field Options tab. + * + * @since 2.7.0 + * + * @param array $type_options Additional field type options. + * @param string $type Field type. + * @param array $options Tabs, indexed by label. + * @param null|\Pods\Whatsit\Pod $pod Pods object for the Pod this UI is for. + */ $type_options = apply_filters( 'pods_admin_setup_edit_additional_field_options', $type_options, $type, $options, $pod ); $options[ 'additional-field-' . $type ] = $type_options; diff --git a/src/Pods/Admin/Config/Pod.php b/src/Pods/Admin/Config/Pod.php index ede48cd659..3f8f5b3a34 100644 --- a/src/Pods/Admin/Config/Pod.php +++ b/src/Pods/Admin/Config/Pod.php @@ -25,6 +25,7 @@ public function get_tabs( \Pods\Whatsit\Pod $pod ) { $admin_ui = false; $connections = false; $advanced = false; + $access = true; $pod_type = pods_v( 'type', $pod ); $is_extended = $pod->is_extended(); @@ -66,6 +67,10 @@ public function get_tabs( \Pods\Whatsit\Pod $pod ) { $core_tabs['advanced'] = __( 'Advanced Options', 'pods' ); } + if ( $access ) { + $core_tabs['access-rights'] = __( 'Access Rights', 'pods' ); + } + // Only include kitchen sink if dev mode on and not running Codecept tests. if ( pods_developer() && ! function_exists( 'codecept_debug' ) ) { $core_tabs['kitchen-sink'] = __( 'Kitchen Sink (temp)', 'pods' ); @@ -137,14 +142,14 @@ public function get_fields( \Pods\Whatsit\Pod $pod, array $tabs ) { 'label' => __( 'Label', 'pods' ), 'help' => __( 'help', 'pods' ), 'type' => 'text', - 'default' => pods_v( 'label', $pod, ucwords( str_replace( '_', ' ', pods_v( 'name', $pod ) ) ) ), + 'default' => pods_v( 'label', $pod, ucwords( str_replace( '_', ' ', $pod_name ) ) ), 'text_max_length' => 30, ], 'label_singular' => [ 'label' => __( 'Singular Label', 'pods' ), 'help' => __( 'help', 'pods' ), 'type' => 'text', - 'default' => pods_v( 'label_singular', $pod, pods_v( 'label', $pod, ucwords( str_replace( '_', ' ', pods_v( 'name', $pod ) ) ) ) ), + 'default' => pods_v( 'label_singular', $pod, pods_v( 'label', $pod, ucwords( str_replace( '_', ' ', $pod_name ) ) ) ), 'text_max_length' => 30, ], 'placeholder_enter_title_here' => [ @@ -640,14 +645,14 @@ public function get_fields( \Pods\Whatsit\Pod $pod, array $tabs ) { 'label' => __( 'Page Title', 'pods' ), 'help' => __( 'help', 'pods' ), 'type' => 'text', - 'default' => str_replace( '_', ' ', pods_v( 'name', $pod ) ), + 'default' => str_replace( '_', ' ', $pod_name ), 'text_max_length' => 30, ], 'menu_name' => [ 'label' => __( 'Menu Name', 'pods' ), 'help' => __( 'help', 'pods' ), 'type' => 'text', - 'default' => pods_v( 'label', $pod, ucwords( str_replace( '_', ' ', pods_v( 'name', $pod ) ) ) ), + 'default' => pods_v( 'label', $pod, ucwords( str_replace( '_', ' ', $pod_name ) ) ), 'text_max_length' => 30, ], ]; @@ -730,7 +735,7 @@ public function get_fields( \Pods\Whatsit\Pod $pod, array $tabs ) { ], ]; - $post_type_name = pods_v( 'name', $pod, 'post_type', true ); + $post_type_name = $pod_name ?: 'post_type'; /** * Allow filtering the default post status. @@ -778,9 +783,10 @@ public function get_fields( \Pods\Whatsit\Pod $pod, array $tabs ) { ]; $options['advanced'] = [ - 'public' => [ + 'public' => [ 'label' => __( 'Public', 'pods' ), - 'help' => __( 'help', 'pods' ), + 'help' => __( 'Regardless of this setting, you can still embed Pods Content and Forms through PHP and make use of other features directly through code.', 'pods' ), + 'description' => __( 'When a content type is public, it can be viewed by anyone when it is embedded through Dynamic Features, WordPress blocks, or through the REST API. Otherwise, a user will need to have the corresponding "read" capability for the content type.', 'pods' ), 'type' => 'boolean', 'default' => true, 'boolean_yes_label' => '', @@ -817,7 +823,7 @@ public function get_fields( \Pods\Whatsit\Pod $pod, array $tabs ) { 'label' => __( 'Custom User Capability', 'pods' ), 'help' => __( 'help', 'pods' ), 'type' => 'text', - 'text_placeholder' => pods_v( 'name', $pod ), + 'text_placeholder' => $pod_name, 'depends-on' => [ 'capability_type' => 'custom' ], ], 'capability_type_extra' => [ @@ -914,13 +920,6 @@ public function get_fields( \Pods\Whatsit\Pod $pod, array $tabs ) { 'default' => true, 'boolean_yes_label' => '', ], - 'can_export' => [ - 'label' => __( 'Exportable', 'pods' ), - 'help' => __( 'help', 'pods' ), - 'type' => 'boolean', - 'default' => true, - 'boolean_yes_label' => '', - ], 'default_status' => [ 'label' => __( 'Default Status', 'pods' ), 'help' => __( 'help', 'pods' ), @@ -1214,11 +1213,19 @@ public function get_fields( \Pods\Whatsit\Pod $pod, array $tabs ) { $options['advanced'] = [ 'public' => [ 'label' => __( 'Public', 'pods' ), - 'help' => __( 'help', 'pods' ), + 'help' => __( 'Regardless of this setting, you can still embed Pods Content and Forms through PHP and make use of other features directly through code.', 'pods' ), + 'description' => __( 'When a content type is public, it can be viewed by anyone when it is embedded through Dynamic Features, WordPress blocks, or through the REST API. Otherwise, a user will need to have the corresponding "read" capability for the content type.', 'pods' ), 'type' => 'boolean', 'default' => true, 'boolean_yes_label' => '', ], + 'publicly_queryable' => [ + 'label' => __( 'Publicly Queryable', 'pods' ), + 'help' => __( 'help', 'pods' ), + 'type' => 'boolean', + 'default' => pods_v( 'public', $pod, true ), + 'boolean_yes_label' => '', + ], 'hierarchical' => [ 'label' => __( 'Hierarchical', 'pods' ), 'help' => __( 'Hierarchical taxonomies will have a list with checkboxes to select an existing category in the taxonomy admin box on the post edit page (like default post categories). Non-hierarchical taxonomies will just have an empty text field to type-in taxonomy terms to associate with the post (like default post tags).', 'pods' ), @@ -1276,7 +1283,7 @@ public function get_fields( \Pods\Whatsit\Pod $pod, array $tabs ) { 'label' => __( 'Custom User Capability', 'pods' ), 'help' => __( 'Enables additional capabilities for this Taxonomy including: manage_{capability}_terms, edit_{capability}_terms, assign_{capability}_terms, and delete_{capability}_terms', 'pods' ), 'type' => 'text', - 'text_placeholder' => pods_v( 'name', $pod ), + 'text_placeholder' => $pod_name, 'depends-on' => [ 'capability_type' => 'custom' ], ], 'query_var' => [ @@ -1286,14 +1293,6 @@ public function get_fields( \Pods\Whatsit\Pod $pod, array $tabs ) { 'default' => false, 'boolean_yes_label' => '', ], - 'query_var' => [ - 'label' => __( 'Query Var', 'pods' ), - 'help' => __( 'help', 'pods' ), - 'type' => 'boolean', - 'default' => false, - 'dependency' => true, - 'boolean_yes_label' => '', - ], 'query_var_string' => [ 'label' => __( 'Custom Query Var Name', 'pods' ), 'help' => __( 'help', 'pods' ), @@ -1422,12 +1421,6 @@ public function get_fields( \Pods\Whatsit\Pod $pod, array $tabs ) { 'depends-on' => [ 'menu_location' => 'top' ], ], ]; - - // @todo fill this in - $options['advanced'] = [ - 'temporary' => 'This type has the fields hardcoded', - // :( - ]; } elseif ( 'pod' === $pod_type ) { $actions_enabled = [ 'add', @@ -1635,14 +1628,14 @@ public function get_fields( \Pods\Whatsit\Pod $pod, array $tabs ) { ]; }//end if + // Add access rights options. + $options['access-rights'] = pods_access_pod_options( $pod_type, $pod_name, $pod ); + // Only include kitchen sink if dev mode on and not running Codecept tests. if ( pods_developer() && ! function_exists( 'codecept_debug' ) ) { $options['kitchen-sink'] = json_decode( file_get_contents( PODS_DIR . 'tests/codeception/_data/kitchen-sink-config.json' ), true ); } - $pod_type = $pod['type']; - $pod_name = $pod['name']; - /** * Add admin fields to the Pods editor for a specific Pod. * diff --git a/src/Pods/Admin/Settings.php b/src/Pods/Admin/Settings.php index 84f8d30e23..d0a9e9b24e 100644 --- a/src/Pods/Admin/Settings.php +++ b/src/Pods/Admin/Settings.php @@ -191,12 +191,17 @@ public function get_setting_fields() { 'default' => '0', 'readonly' => $is_types_only_overridden, 'description' => $is_types_only_overridden ? $is_types_only_disabled_text : '', - 'pick_format' => 'single', + 'pick_format_type' => 'single', 'pick_format_single' => 'radio', 'data' => [ '0' => __( 'Enable creating custom fields with Pods', 'pods' ), '1' => __( 'Disable creating custom fields with Pods (for when using Pods only for content types)', 'pods' ), ], + 'site_health_data' => [ + '0' => __( 'Enable', 'pods' ), + '1' => __( 'Disable', 'pods' ), + ], + 'site_health_include_in_info' => true, ]; $fields['performance'] = [ @@ -212,13 +217,18 @@ public function get_setting_fields() { 'label' => __( 'Watch changed fields for use in hooks', 'pods' ), 'help' => __( 'By default, Pods does not watch changed fields when a post, term, user, or other Pods items are saved. Enabling this will allow you to use PHP hooks to reference the previous values of those fields after the save has happened.', 'pods' ), 'type' => 'pick', - 'default' => version_compare( $first_pods_version, '2.8.21', '<=' ) ? '1' : '0', - 'pick_format' => 'single', + 'default' => version_compare( $first_pods_version, '2.8.21', '<' ) ? '1' : '0', + 'pick_format_type' => 'single', 'pick_format_single' => 'radio', 'data' => [ '1' => __( 'Enable watching changed fields (may reduce performance with large processes)', 'pods' ), '0' => __( 'Disable watching changed fields', 'pods' ), ], + 'site_health_data' => [ + '1' => __( 'Enable', 'pods' ), + '0' => __( 'Disable', 'pods' ), + ], + 'site_health_include_in_info' => true, ]; $fields['metadata_integration'] = [ @@ -226,14 +236,19 @@ public function get_setting_fields() { 'label' => __( 'Watch WP Metadata calls', 'pods' ), 'help' => __( 'By default, Pods will watch Metadata calls and send any values to table-based fields as well as index relationship IDs when they are saved. You can disable this if you do not use table-based Pods and you only want to query meta-based Pods or settings.', 'pods' ), 'type' => 'pick', - 'default' => ! function_exists( 'wc_get_product' ) || version_compare( $first_pods_version, '2.9.14', '<' ) ? '1' : '0', - 'pick_format' => 'single', + 'default' => ( function_exists( 'wc_get_product' ) || version_compare( $first_pods_version, '2.9.14', '<' ) ) ? '1' : '0', + 'pick_format_type' => 'single', 'pick_format_single' => 'radio', 'data' => [ '1' => __( 'Enable watching WP Metadata calls (may reduce performance with large processes)', 'pods' ), '0' => __( 'Disable watching WP Metadata calls', 'pods' ), ], + 'site_health_data' => [ + '1' => __( 'Enable', 'pods' ), + '0' => __( 'Disable', 'pods' ), + ], 'dependency' => true, + 'site_health_include_in_info' => true, ]; $fields['metadata_override_get'] = [ @@ -241,14 +256,19 @@ public function get_setting_fields() { 'label' => __( 'Override WP Metadata values', 'pods' ), 'help' => __( 'By default, Pods will override Metadata values when calling functions like get_post_meta() so that it can provide more Relationship / File field context.', 'pods' ), 'type' => 'pick', - 'default' => version_compare( $first_pods_version, '2.8.21', '<=' ) ? '1' : '0', - 'pick_format' => 'single', + 'default' => version_compare( $first_pods_version, '2.8.21', '<' ) ? '1' : '0', + 'pick_format_type' => 'single', 'pick_format_single' => 'radio', 'data' => [ '1' => __( 'Enable overriding WP Metadata values (may conflict with certain plugins and decrease performance with large processes)', 'pods' ), '0' => __( 'Disable overriding WP Metadata values', 'pods' ), ], - 'depends-on' => [ 'metadata_integration' => '1' ], + 'site_health_data' => [ + '1' => __( 'Enable', 'pods' ), + '0' => __( 'Disable', 'pods' ), + ], + 'depends-on' => [ 'metadata_integration' => '1' ], + 'site_health_include_in_info' => true, ]; $fields['media_modal_fields'] = [ @@ -256,20 +276,25 @@ public function get_setting_fields() { 'label' => __( 'Show Pods fields in Media Library modals', 'pods' ), 'help' => __( 'This feature is only used when you have extended the WordPress Media object with Pods', 'pods' ), 'type' => 'pick', - 'default' => version_compare( $first_pods_version, '2.9.16', '<=' ) ? '1' : '0', - 'pick_format' => 'single', + 'default' => version_compare( $first_pods_version, '2.9.16', '<' ) ? '1' : '0', + 'pick_format_type' => 'single', 'pick_format_single' => 'radio', 'data' => [ '1' => __( 'Enable showing Pods fields in Media Library modals (may decrease performance with large numbers of items on admin screens with media grids)', 'pods' ), '0' => __( 'Disable showing Pods fields in Media Library modals and only show them when in the full edit screen for an attachment', 'pods' ), ], + 'site_health_data' => [ + '0' => __( 'Enable', 'pods' ), + '1' => __( 'Disable', 'pods' ), + ], + 'site_health_include_in_info' => true, ]; $session_auto_start = pods_session_auto_start( true ); $session_auto_start_overridden = null !== $session_auto_start; - $fields['sessions'] = [ - 'label' => __( 'Sessions', 'pods' ), + $fields['security'] = [ + 'label' => __( 'Security', 'pods' ), 'type' => 'heading', ]; @@ -288,15 +313,27 @@ public function get_setting_fields() { 'default' => '0', 'readonly' => $session_auto_start_overridden, 'description' => $session_auto_start_overridden ? $session_auto_start_disabled_text : '', - 'pick_format' => 'single', + 'pick_format_type' => 'single', 'pick_format_single' => 'radio', 'data' => [ 'auto' => __( 'Auto-detect sessions (enable on first anonymous submission)', 'pods' ), '1' => __( 'Enable sessions (may decrease performance)', 'pods' ), '0' => __( 'Disable sessions', 'pods' ), ], + 'site_health_data' => [ + 'auto' => __( 'Auto-detect', 'pods' ), + '1' => __( 'Enable', 'pods' ), + '0' => __( 'Disable', 'pods' ), + ], + 'site_health_include_in_info' => true, ]; + $access_fields = pods_access_settings_config(); + + if ( $access_fields ) { + $fields = array_merge( $fields, $access_fields ); + } + $pods_init = pods_init(); $is_wisdom_opted_out = ! $pods_init->stats_tracking || ! $pods_init->stats_tracking->get_is_tracking_allowed(); @@ -310,13 +347,15 @@ public function get_setting_fields() { $fields['wisdom_opt_out'] = [ 'name' => 'wisdom_opt_out', 'label' => __( 'Would you like to opt-out of tracking?', 'pods' ), - 'description' => __( 'Thank you for installing our plugin. We\'d like your permission to track its usage on your site. We won\'t record any sensitive data, only information regarding the WordPress environment and your plugin settings. We will only use this information help us make improvements to the plugin and provide better support when you reach out. Tracking is completely optional.', 'pods' ), + 'description' => __( 'Thank you for installing our plugin. We\'d like your permission to track its usage on your site. We won\'t record any sensitive data, only information regarding the WordPress environment and your plugin settings. We will only use this information help us make improvements to the plugin and provide better support when you reach out. Tracking is completely optional.', 'pods' ) + . "\n\n" + . __( 'Any information collected is not shared with third-parties and you will not be signed up for mailing lists.', 'pods' ), 'type' => 'pick', 'default' => $is_wisdom_opted_out ? '1' : '', - 'pick_format' => 'single', + 'pick_format_type' => 'single', 'pick_format_single' => 'radio', 'data' => [ - '' => __( 'Track usage on my site', 'pods' ), + '' => __( 'Track usage on my site', 'pods' ), '1' => __( 'DO NOT track usage on my site', 'pods' ), ], ]; diff --git a/src/Pods/Blocks/API.php b/src/Pods/Blocks/API.php index 1ffb19c582..fec956bca6 100644 --- a/src/Pods/Blocks/API.php +++ b/src/Pods/Blocks/API.php @@ -169,12 +169,24 @@ public function setup_core_blocks() { do_action( 'pods_blocks_api_pre_init' ); pods_container( 'pods.blocks.collection.pods' ); - pods_container( 'pods.blocks.field' ); - pods_container( 'pods.blocks.form' ); - pods_container( 'pods.blocks.list' ); - pods_container( 'pods.blocks.single' ); - pods_container( 'pods.blocks.single-list-fields' ); - pods_container( 'pods.blocks.view' ); + + // Check if the feature is enabled. + if ( pods_can_use_dynamic_feature( 'display' ) ) { + pods_container( 'pods.blocks.field' ); + pods_container( 'pods.blocks.list' ); + pods_container( 'pods.blocks.single' ); + pods_container( 'pods.blocks.single-list-fields' ); + } + + // Check if the feature is enabled. + if ( pods_can_use_dynamic_feature( 'form' ) ) { + pods_container( 'pods.blocks.form' ); + } + + // Check if the feature is enabled. + if ( pods_can_use_dynamic_feature( 'view' ) ) { + pods_container( 'pods.blocks.view' ); + } /** * Allow custom blocks to be registered with Pods. diff --git a/src/Pods/Blocks/Types/Base.php b/src/Pods/Blocks/Types/Base.php index 5fe228aa3e..5891d67b18 100644 --- a/src/Pods/Blocks/Types/Base.php +++ b/src/Pods/Blocks/Types/Base.php @@ -123,6 +123,10 @@ public function attributes( $params = [] ) { } } + $params['_is_editor_mode'] = $this->in_editor_mode( $params ); + $params['_is_preview'] = is_preview(); + $params['_preview_id'] = $params['_is_preview'] ? get_queried_object_id() : null; + return parent::attributes( $params ); } @@ -245,7 +249,7 @@ public function should_preload_block( $attributes = [], $block = null ) { * @return bool Whether the block is being rendered in editor mode. */ public function in_editor_mode( $attributes = [] ) { - if ( ! empty( $attributes['_is_editor'] ) ) { + if ( ! empty( $attributes['_is_editor'] ) && ! empty( $attributes['_is_editor_mode'] ) ) { return true; } diff --git a/src/Pods/Blocks/Types/Field.php b/src/Pods/Blocks/Types/Field.php index f5538f80d1..48fe3f071d 100644 --- a/src/Pods/Blocks/Types/Field.php +++ b/src/Pods/Blocks/Types/Field.php @@ -101,6 +101,17 @@ public function fields() { 'default' => '', 'description' => __( 'Choose the pod to reference, or reference the Pod in the current context of this block.', 'pods' ), ], + [ + 'name' => 'access_rights_help', + 'label' => __( 'Access Rights', 'pods' ), + 'type' => 'html', + 'default' => '', + 'html_content' => sprintf( + // translators: %s is the Read Documentation link. + esc_html__( 'Read about how access rights control what can be displayed to other users: %s', 'pods' ), + '' . esc_html__( 'Documentation', 'pods' ) . '' + ), + ], [ 'name' => 'slug', 'label' => __( 'Slug or ID', 'pods' ), @@ -128,6 +139,11 @@ public function fields() { * @return string The block content to render. */ public function render( $attributes = [], $content = '', $block = null ) { + // If the feature is disabled then return early. + if ( ! pods_can_use_dynamic_feature( 'display' ) ) { + return ''; + } + $attributes = $this->attributes( $attributes ); $attributes = array_map( 'pods_trim', $attributes ); @@ -154,7 +170,8 @@ public function render( $attributes = [], $content = '', $block = null ) { $attributes['use_current'] = false; } - $provided_post_id = absint( pods_v( '_post_id', $attributes, pods_v( 'post_id', 'get', 0, true ), true ) ); + $provided_post_id = $this->in_editor_mode( $attributes ) ? pods_v( 'post_id', 'get', 0, true ) : get_the_ID(); + $provided_post_id = absint( pods_v( '_post_id', $attributes, $provided_post_id, true ) ); if ( $attributes['use_current'] && $block instanceof WP_Block && ! empty( $block->context['postType'] ) ) { // Detect post type / ID from context. diff --git a/src/Pods/Blocks/Types/Form.php b/src/Pods/Blocks/Types/Form.php index d74c2cbcca..2da91a90cc 100644 --- a/src/Pods/Blocks/Types/Form.php +++ b/src/Pods/Blocks/Types/Form.php @@ -51,39 +51,44 @@ public function block() { 'transforms' => [ 'from' => [ [ - 'type' => 'shortcode', - 'tag' => 'pods-form', - 'attributes' => [ - 'name' => [ + 'type' => 'shortcode', + 'tag' => 'pods-form', + 'attributes' => [ + 'name' => [ 'type' => 'object', 'source' => 'shortcode', 'attribute' => 'name', ], - 'slug' => [ + 'slug' => [ 'type' => 'string', 'source' => 'shortcode', 'attribute' => 'slug', ], - 'fields' => [ + 'fields' => [ 'type' => 'string', 'source' => 'shortcode', 'attribute' => 'fields', ], - 'label' => [ + 'label' => [ 'type' => 'string', 'source' => 'shortcode', 'attribute' => 'label', ], - 'thank_you' => [ + 'thank_you' => [ 'type' => 'string', 'source' => 'shortcode', 'attribute' => 'thank_you', ], - 'form_output_type' => [ + 'form_output_type' => [ 'type' => 'object', 'source' => 'shortcode', 'attribute' => 'form_output_type', ], + 'form_key' => [ + 'type' => 'string', + 'source' => 'shortcode', + 'attribute' => 'form_key', + ], ], 'isMatchConfig' => [ [ @@ -93,39 +98,44 @@ public function block() { ], ], [ - 'type' => 'shortcode', - 'tag' => 'pods', - 'attributes' => [ - 'name' => [ + 'type' => 'shortcode', + 'tag' => 'pods', + 'attributes' => [ + 'name' => [ 'type' => 'object', 'source' => 'shortcode', 'attribute' => 'name', ], - 'slug' => [ + 'slug' => [ 'type' => 'string', 'source' => 'shortcode', 'attribute' => 'slug', ], - 'fields' => [ + 'fields' => [ 'type' => 'string', 'source' => 'shortcode', 'attribute' => 'fields', ], - 'label' => [ + 'label' => [ 'type' => 'string', 'source' => 'shortcode', 'attribute' => 'label', ], - 'thank_you' => [ + 'thank_you' => [ 'type' => 'string', 'source' => 'shortcode', 'attribute' => 'thank_you', ], - 'form_output_type' => [ + 'form_output_type' => [ 'type' => 'object', 'source' => 'shortcode', 'attribute' => 'form_output_type', ], + 'form_key' => [ + 'type' => 'string', + 'source' => 'shortcode', + 'attribute' => 'form_key', + ], ], 'isMatchConfig' => [ [ @@ -156,6 +166,17 @@ public function fields() { 'default' => '', 'description' => __( 'Choose the pod to reference, or reference the Pod in the current context of this block.', 'pods' ), ], + [ + 'name' => 'access_rights_help', + 'label' => __( 'Access Rights', 'pods' ), + 'type' => 'html', + 'default' => '', + 'html_content' => sprintf( + // translators: %s is the Read Documentation link. + esc_html__( 'Read about how access rights control what can be displayed to other users: %s', 'pods' ), + '' . esc_html__( 'Documentation', 'pods' ) . '' + ), + ], [ 'name' => 'slug', 'label' => __( 'Slug or ID', 'pods' ), @@ -193,6 +214,23 @@ public function fields() { 'default' => 'div', 'description' => __( 'Choose how you want your form HTML to be set up. This allows you flexibility to build and style your forms with any CSS customizations you would like. Some output types are naturally laid out better in certain themes.', 'pods' ), ], + [ + 'name' => 'form_key', + 'label' => __( 'Form Access Key', 'pods' ), + 'type' => 'text', + 'description' => __( 'Give this form a unique slug to reference in our user access checks. When you use the "pods_user_can_access_object" filter, you can selectively allow access through PHP if the user does not normally have access through WordPress itself.', 'pods' ), + ], + [ + 'name' => 'access_rights_form_key_help', + 'label' => __( 'Access Rights', 'pods' ), + 'type' => 'html', + 'default' => '', + 'html_content' => sprintf( + // translators: %s is the Read Documentation link. + esc_html__( 'Find out more about how to customize access rights for this form: %s', 'pods' ), + '' . esc_html__( 'Documentation', 'pods' ) . '' + ), + ], ]; } @@ -208,6 +246,11 @@ public function fields() { * @return string The block content to render. */ public function render( $attributes = [], $content = '', $block = null ) { + // If the feature is disabled then return early. + if ( ! pods_can_use_dynamic_feature( 'form' ) ) { + return ''; + } + $attributes = $this->attributes( $attributes ); $attributes = array_map( 'pods_trim', $attributes ); diff --git a/src/Pods/Blocks/Types/Item_List.php b/src/Pods/Blocks/Types/Item_List.php index 1061d38dd0..de13810fd0 100644 --- a/src/Pods/Blocks/Types/Item_List.php +++ b/src/Pods/Blocks/Types/Item_List.php @@ -205,7 +205,7 @@ public function fields() { */ $default_cache_mode = apply_filters( 'pods_shortcode_default_cache_mode', 'none' ); - return [ + $fields = [ [ 'name' => 'name', 'label' => __( 'Pod Name', 'pods' ), @@ -214,6 +214,17 @@ public function fields() { 'default' => '', 'description' => __( 'Choose the pod to reference, or reference the Pod in the current context of this block.', 'pods' ), ], + [ + 'name' => 'access_rights_help', + 'label' => __( 'Access Rights', 'pods' ), + 'type' => 'html', + 'default' => '', + 'html_content' => sprintf( + // translators: %s is the Read Documentation link. + esc_html__( 'Read about how access rights control what can be displayed to other users: %s', 'pods' ), + '' . esc_html__( 'Documentation', 'pods' ) . '' + ), + ], [ 'name' => 'template', 'label' => __( 'Template', 'pods' ), @@ -342,6 +353,13 @@ public function fields() { 'description' => __( 'Set how long to cache the output for in seconds.', 'pods' ), ], ]; + + if ( ! pods_can_use_dynamic_feature_sql_clauses() ) { + unset( $fields['orderby'] ); + unset( $fields['where'] ); + } + + return $fields; } /** @@ -356,6 +374,11 @@ public function fields() { * @return string The block content to render. */ public function render( $attributes = [], $content = '', $block = null ) { + // If the feature is disabled then return early. + if ( ! pods_can_use_dynamic_feature( 'display' ) ) { + return ''; + } + $attributes = $this->attributes( $attributes ); $attributes = array_map( 'pods_trim', $attributes ); @@ -380,7 +403,8 @@ public function render( $attributes = [], $content = '', $block = null ) { $attributes['name'] = $block->context['postType']; } - $provided_post_id = absint( pods_v( '_post_id', $attributes, pods_v( 'post_id', 'get', 0, true ), true ) ); + $provided_post_id = $this->in_editor_mode( $attributes ) ? pods_v( 'post_id', 'get', 0, true ) : get_the_ID(); + $provided_post_id = absint( pods_v( '_post_id', $attributes, $provided_post_id, true ) ); if ( empty( $attributes['name'] ) ) { if ( diff --git a/src/Pods/Blocks/Types/Item_Single.php b/src/Pods/Blocks/Types/Item_Single.php index 5e5d00795c..dee82c9052 100644 --- a/src/Pods/Blocks/Types/Item_Single.php +++ b/src/Pods/Blocks/Types/Item_Single.php @@ -170,6 +170,17 @@ public function fields() { 'default' => '', 'description' => __( 'Choose the pod to reference, or reference the Pod in the current context of this block.', 'pods' ), ], + [ + 'name' => 'access_rights_help', + 'label' => __( 'Access Rights', 'pods' ), + 'type' => 'html', + 'default' => '', + 'html_content' => sprintf( + // translators: %s is the Read Documentation link. + esc_html__( 'Read about how access rights control what can be displayed to other users: %s', 'pods' ), + '' . esc_html__( 'Documentation', 'pods' ) . '' + ), + ], [ 'name' => 'slug', 'label' => __( 'Slug or ID', 'pods' ), @@ -205,6 +216,11 @@ public function fields() { * @return string The block content to render. */ public function render( $attributes = [], $content = '', $block = null ) { + // If the feature is disabled then return early. + if ( ! pods_can_use_dynamic_feature( 'display' ) ) { + return ''; + } + $attributes = $this->attributes( $attributes ); $attributes = array_map( 'pods_trim', $attributes ); @@ -234,7 +250,8 @@ public function render( $attributes = [], $content = '', $block = null ) { $attributes['use_current'] = false; } - $provided_post_id = absint( pods_v( '_post_id', $attributes, pods_v( 'post_id', 'get', 0, true ), true ) ); + $provided_post_id = $this->in_editor_mode( $attributes ) ? pods_v( 'post_id', 'get', 0, true ) : get_the_ID(); + $provided_post_id = absint( pods_v( '_post_id', $attributes, $provided_post_id, true ) ); if ( $attributes['use_current'] && $block instanceof WP_Block && ! empty( $block->context['postType'] ) ) { // Detect post type / ID from context. diff --git a/src/Pods/Blocks/Types/Item_Single_List_Fields.php b/src/Pods/Blocks/Types/Item_Single_List_Fields.php index f1adcda4a3..b289469e72 100644 --- a/src/Pods/Blocks/Types/Item_Single_List_Fields.php +++ b/src/Pods/Blocks/Types/Item_Single_List_Fields.php @@ -70,6 +70,17 @@ public function fields() { 'default' => '', 'description' => __( 'Choose the pod to reference, or reference the Pod in the current context of this block.', 'pods' ), ], + [ + 'name' => 'access_rights_help', + 'label' => __( 'Access Rights', 'pods' ), + 'type' => 'html', + 'default' => '', + 'html_content' => sprintf( + // translators: %s is the Read Documentation link. + esc_html__( 'Read about how access rights control what can be displayed to other users: %s', 'pods' ), + '' . esc_html__( 'Documentation', 'pods' ) . '' + ), + ], [ 'name' => 'slug', 'label' => __( 'Slug or ID', 'pods' ), @@ -117,6 +128,11 @@ public function fields() { * @return string The block content to render. */ public function render( $attributes = [], $content = '', $block = null ) { + // If the feature is disabled then return early. + if ( ! pods_can_use_dynamic_feature( 'display' ) ) { + return ''; + } + $attributes = $this->attributes( $attributes ); $attributes = array_map( 'pods_trim', $attributes ); diff --git a/src/Pods/Blocks/Types/View.php b/src/Pods/Blocks/Types/View.php index 19c582ba65..1deedd1279 100644 --- a/src/Pods/Blocks/Types/View.php +++ b/src/Pods/Blocks/Types/View.php @@ -139,6 +139,11 @@ public function fields() { * @return string The block content to render. */ public function render( $attributes = [], $content = '', $block = null ) { + // If the feature is disabled then return early. + if ( ! pods_can_use_dynamic_feature( 'view' ) ) { + return ''; + } + $attributes = $this->attributes( $attributes ); $attributes = array_map( 'pods_trim', $attributes ); diff --git a/src/Pods/Data/Conditional_Logic.php b/src/Pods/Data/Conditional_Logic.php index 5ce26f5b93..9f3eda81b5 100644 --- a/src/Pods/Data/Conditional_Logic.php +++ b/src/Pods/Data/Conditional_Logic.php @@ -128,7 +128,7 @@ public static function maybe_setup_from_old_syntax( $object ): ?Conditional_Logi if ( empty( $old_syntax_value ) ) { $old_syntax_value = []; } elseif ( ! is_array( $old_syntax_value ) ) { - $old_syntax_value = maybe_unserialize( $old_syntax_value ); + $old_syntax_value = pods_maybe_safely_unserialize( $old_syntax_value ); if ( ! is_array( $old_syntax_value ) ) { $old_syntax_value = []; diff --git a/src/Pods/Data/Map_Field_Values.php b/src/Pods/Data/Map_Field_Values.php index 06c79b8622..9d3912bd0a 100644 --- a/src/Pods/Data/Map_Field_Values.php +++ b/src/Pods/Data/Map_Field_Values.php @@ -397,10 +397,6 @@ public function context_info( $field, $traverse, $field_data, $obj ) { $raw = isset( $traverse[2] ) && 'raw' === $traverse[2]; - if ( 'user' === $context_type && 'user_pass' === $context_var ) { - return ''; - } - $value = pods_v( $context_var, $context_type ); // Maybe return the raw value. diff --git a/src/Pods/Permissions.php b/src/Pods/Permissions.php index c93456011c..01e37008e8 100644 --- a/src/Pods/Permissions.php +++ b/src/Pods/Permissions.php @@ -166,7 +166,7 @@ public function get_restricted_roles( $object ) { $roles_allowed = pods_v( 'roles_allowed', $object, '' ); if ( '' !== $roles_allowed ) { - $roles_allowed = maybe_unserialize( $roles_allowed ); + $roles_allowed = pods_maybe_safely_unserialize( $roles_allowed ); if ( ! is_array( $roles_allowed ) ) { $roles_allowed = explode( ',', $roles_allowed ); @@ -245,7 +245,7 @@ public function get_restricted_capabilities( $object ) { $capability_allowed = pods_v( 'capability_allowed', $object, '' ); if ( '' !== $capability_allowed ) { - $capability_allowed = maybe_unserialize( $capability_allowed ); + $capability_allowed = pods_maybe_safely_unserialize( $capability_allowed ); if ( ! is_array( $capability_allowed ) ) { $capability_allowed = explode( ',', $capability_allowed ); diff --git a/src/Pods/Tools/Repair.php b/src/Pods/Tools/Repair.php index 0fe579c2c9..c113810d81 100644 --- a/src/Pods/Tools/Repair.php +++ b/src/Pods/Tools/Repair.php @@ -16,6 +16,158 @@ */ class Repair extends Base { + /** + * Repair Pod configurations. + * + * @since 3.1.0 + * + * @param string $mode The repair mode (preview, upgrade, or full). + * + * @return array The results with information about the repair done. + */ + public function repair_pods( $mode ) { + $this->setup(); + + $this->errors = []; + + $is_preview_mode = 'preview' === $mode; + + $results = []; + + $results[ __( 'Check for duplicate Pods in the database', 'pods' ) ] = $this->maybe_resolve_pod_conflicts( $mode ); + + // Check if changes were made to the Pod. + $changes_made = [] !== array_filter( $results ); + + if ( ! $is_preview_mode && $changes_made ) { + $this->api->cache_flush_pods(); + } + + $tool_heading = __( 'Repair results', 'pods' ); + + $results['message_html'] = $this->get_message_html( $tool_heading, $results, $mode ); + + return $results; + } + + /** + * Maybe resolve pod conflicts. + * + * @since 3.1.0 + * + * @param string $mode The repair mode (preview, upgrade, or full). + * + * @return string[] The label, name, and ID for each pod resolved. + */ + protected function maybe_resolve_pod_conflicts( $mode ) { + $this->setup(); + + // Find any pod that has the same name as another pod. + global $wpdb; + + $sql = " + SELECT DISTINCT + `primary`.`ID` AS `primary_id`, + `primary`.`post_name` AS `primary_name`, + `duplicate`.`ID` AS `duplicate_id`, + `duplicate`.`post_name` AS `duplicate_name` + FROM `{$wpdb->posts}` AS `primary` + LEFT JOIN `{$wpdb->posts}` AS `duplicate` + ON `duplicate`.`post_name` = `primary`.`post_name` + WHERE + `primary`.`post_type` = %s + AND `duplicate`.`ID` != `primary`.`ID` + AND `duplicate`.`post_type` = `primary`.`post_type` + ORDER BY `primary`.`ID` + "; + + $duplicate_pods = $wpdb->get_results( + $wpdb->prepare( + $sql, + [ + '_pods_pod', + ] + ) + ); + + $pods_to_resolve = []; + + $duplicate_ids = []; + + foreach ( $duplicate_pods as $duplicate_pod ) { + // Skip if we have already referenced a primary pod. + if ( + isset( $duplicate_ids[ $duplicate_pod->duplicate_id ] ) + && $duplicate_ids[ $duplicate_pod->duplicate_id ] === $duplicate_pod->primary_id + ) { + continue; + } + + $duplicate_ids[ $duplicate_pod->primary_id ] = $duplicate_pod->duplicate_id; + + if ( ! isset( $pods_to_resolve[ $duplicate_pod->primary_name ] ) ) { + $pods_to_resolve[ $duplicate_pod->primary_name ] = []; + } + + try { + $pod = $this->api->load_pod( [ 'id' => $duplicate_pod->duplicate_id ] ); + + if ( $pod ) { + $pods_to_resolve[ $duplicate_pod->primary_name ][] = $pod; + } else { + throw new Exception( __( 'Failed to load duplicate pod to resolve.', 'pods' ) ); + } + } catch ( Throwable $exception ) { + $this->errors[] = ucwords( str_replace( '_', ' ', __FUNCTION__ ) ) . ' > ' . $exception->getMessage() . ' (' . $duplicate_pod->duplicate_name . ' - #' . $duplicate_pod->duplicate_id . ' - Primary: ' . $duplicate_pod->primary_name . ' - #' . $duplicate_pod->primary_id . ')'; + } + } + + $resolved_pods = []; + + foreach ( $pods_to_resolve as $primary_pod_name => $pods ) { + foreach ( $pods as $pod ) { + /** @var Pod $pod */ + try { + if ( 'preview' !== $mode ) { + // Prevent renaming the original pod data by using a temp one first, then renaming that. + wp_update_post( [ + 'ID' => $pod->get_id(), + 'post_name' => '_temp_' . $primary_pod_name . '_' . $pod->get_id(), + ] ); + + // Flush the pod cache. + $this->api->cache_flush_pods(); + + // Save the pod with the new name. + $this->api->save_pod( [ + 'id' => $pod->get_id(), + 'old_name' => '_temp_' . $primary_pod_name . '_' . $pod->get_id(), + 'name' => $primary_pod_name . '_' . $pod->get_id(), + 'label' => $pod->get_label() . ' (' . $pod->get_id() . ')', + ], false ); + + $pod->flush(); + } + + $resolved_pods[] = sprintf( + '%1$s (%2$s: %3$s | %4$s: %5$s | %6$s: %7$d)', + $pod->get_label(), + __( 'Old Name', 'pods' ), + $primary_pod_name, + __( 'New Name', 'pods' ), + $primary_pod_name . '_' . $pod->get_id(), + __( 'ID', 'pods' ), + $pod->get_id() + ); + } catch ( Throwable $exception ) { + $this->errors[] = ucwords( str_replace( '_', ' ', __FUNCTION__ ) ) . ' > ' . $exception->getMessage() . ' (' . $pod->get_name() . ' - #' . $pod->get_id() . ')'; + } + } + } + + return $resolved_pods; + } + /** * Repair Groups and Fields for a Pod. * diff --git a/src/Pods/Whatsit.php b/src/Pods/Whatsit.php index d85b225d88..0ceabc402a 100644 --- a/src/Pods/Whatsit.php +++ b/src/Pods/Whatsit.php @@ -122,7 +122,7 @@ public function __construct( array $args = [] ) { * @return Whatsit|array|null */ public static function from_serialized( $serialized, $to_args = false ) { - $object = maybe_unserialize( $serialized ); + $object = pods_maybe_safely_unserialize( $serialized ); if ( $object instanceof self ) { if ( $to_args ) { diff --git a/src/Pods/Whatsit/Block_Field.php b/src/Pods/Whatsit/Block_Field.php index fd0cf7a42f..0e7c3ea0aa 100644 --- a/src/Pods/Whatsit/Block_Field.php +++ b/src/Pods/Whatsit/Block_Field.php @@ -92,6 +92,16 @@ protected function get_block_arg_mapping() { 'type' => 'string', ], ], + 'html' => [ + 'type' => 'html', + 'fieldOptions' => [ + 'className' => 'text__container', + 'type' => 'text', + ], + 'attributeOptions' => [ + 'type' => 'string', + ], + ], ]; } @@ -148,6 +158,10 @@ public function get_block_args() { $block_args['fieldOptions']['help'] = $this->get_arg( 'description' ); + if ( 'html' === $type ) { + $block_args['fieldOptions']['html_content'] = $this->get_arg( 'html_content' ); + } + if ( 'boolean' !== $type ) { $block_args['fieldOptions']['label'] = $this->get_arg( 'label' ); diff --git a/src/Pods/Whatsit/Storage/Post_Type.php b/src/Pods/Whatsit/Storage/Post_Type.php index 8d35f3d64a..1ce25c9dc9 100644 --- a/src/Pods/Whatsit/Storage/Post_Type.php +++ b/src/Pods/Whatsit/Storage/Post_Type.php @@ -724,7 +724,7 @@ public function get_args( Whatsit $object ) { continue; } - $meta_value = array_map( 'maybe_unserialize', $meta_value ); + $meta_value = array_map( 'pods_maybe_safely_unserialize', $meta_value ); if ( 1 === count( $meta_value ) ) { $meta_value = reset( $meta_value ); diff --git a/tests/codeception/wpunit/Pods/API/ProcessFormTest.php b/tests/codeception/wpunit/Pods/API/ProcessFormTest.php index 23737671c5..46dbfc16a0 100644 --- a/tests/codeception/wpunit/Pods/API/ProcessFormTest.php +++ b/tests/codeception/wpunit/Pods/API/ProcessFormTest.php @@ -88,12 +88,30 @@ class ProcessFormTest extends Pods_UnitTestCase { */ protected $field_conditional_hide_id; + protected $test_blocks; + public function setUp(): void { parent::setUp(); $this->api = pods_api(); $this->populate(); + + $this->test_blocks = ' +

    Test block here with bold text

    + + + +
    +
    +
    + + + + +'; } /** @@ -453,6 +471,55 @@ public function test_process_form_returns_valid_id() { $pod = pods( $this->pod ); $params = $this->create_base_form_params( $pod, [ + 'post_content', + $this->field, + $this->field2, + $this->field_hidden, + $this->field_conditional_show, + $this->field_conditional_hide, + ] ); + + // Set some data that will be saved. + $params[ 'pods_field_post_content' ] = '[pods]test[/pods] some text here ' . $this->test_blocks; + + $params[ 'pods_field_' . $this->field ] = 'some value for the field: ' . $this->field; + $params[ 'pods_field_' . $this->field2 ] = 'some value for the field: ' . $this->field2; + $params[ 'pods_field_' . $this->field_hidden ] = 'some value for the field: ' . $this->field_hidden; + $params[ 'pods_field_' . $this->field_conditional_show ] = 'some value for the field: ' . $this->field_conditional_show; + $params[ 'pods_field_' . $this->field_conditional_hide ] = 'some value for the field: ' . $this->field_conditional_hide; + + $id = $this->api->process_form( $params ); + + $this->assertIsInt( $id ); + $this->assertGreaterThan( 0, $id ); + + $pod->fetch( $id ); + + $this->assertEquals( ' some text here ' . $this->test_blocks, $pod->field( 'post_content' ) ); + $this->assertEquals( $params[ 'pods_field_' . $this->field ], $pod->field( $this->field ) ); + $this->assertEquals( $params[ 'pods_field_' . $this->field2 ], $pod->field( $this->field2 ) ); + $this->assertEquals( $params[ 'pods_field_' . $this->field_hidden ], $pod->field( $this->field_hidden ) ); + $this->assertEquals( '', $pod->field( $this->field_conditional_show ) ); // This field would be hidden and won't save. + $this->assertEquals( $params[ 'pods_field_' . $this->field_conditional_hide ], $pod->field( $this->field_conditional_hide ) ); + } + + public function test_process_form_returns_valid_id_and_restricts_post_content_submitted() { + $user_id = wp_insert_user( [ + 'user_login' => 'testsubscriber', + 'user_email' => 'testsubscriber@test.local', + 'user_pass' => 'hayyyyyy', + 'role' => 'subscriber', + ] ); + + $this->assertIsInt( $user_id ); + + wp_set_current_user( $user_id ); + + /** @var Pods $pod */ + $pod = pods( $this->pod ); + + $params = $this->create_base_form_params( $pod, [ + 'post_content', $this->field, $this->field2, $this->field_hidden, @@ -461,6 +528,8 @@ public function test_process_form_returns_valid_id() { ] ); // Set some data that will be saved. + $params[ 'pods_field_post_content' ] = '[pods]test[/pods]' . $this->test_blocks; + $params[ 'pods_field_' . $this->field ] = 'some value for the field: ' . $this->field; $params[ 'pods_field_' . $this->field2 ] = 'some value for the field: ' . $this->field2; $params[ 'pods_field_' . $this->field_hidden ] = 'some value for the field: ' . $this->field_hidden; @@ -474,6 +543,7 @@ public function test_process_form_returns_valid_id() { $pod->fetch( $id ); + $this->assertEquals( '

    Test block here with bold text

    ', $pod->field( 'post_content' ) ); $this->assertEquals( $params[ 'pods_field_' . $this->field ], $pod->field( $this->field ) ); $this->assertEquals( $params[ 'pods_field_' . $this->field2 ], $pod->field( $this->field2 ) ); $this->assertEquals( $params[ 'pods_field_' . $this->field_hidden ], $pod->field( $this->field_hidden ) ); diff --git a/tests/codeception/wpunit/Pods/AdminTest.php b/tests/codeception/wpunit/Pods/AdminTest.php index ba978f6966..b1d7b06d81 100644 --- a/tests/codeception/wpunit/Pods/AdminTest.php +++ b/tests/codeception/wpunit/Pods/AdminTest.php @@ -215,6 +215,7 @@ public function provider_global_config_checks() { 'admin-ui', 'connections', 'advanced', + 'access-rights', 'pods-pfat', 'rest-api', ], @@ -243,6 +244,7 @@ public function provider_global_config_checks() { 'admin-ui', 'connections', 'advanced', + 'access-rights', 'pods-pfat', 'rest-api', ], @@ -268,6 +270,7 @@ public function provider_global_config_checks() { 'config' => [ 'pod' => [ 'groups' => [ + 'access-rights', 'pods-pfat', 'rest-api', ], @@ -293,6 +296,7 @@ public function provider_global_config_checks() { 'config' => [ 'pod' => [ 'groups' => [ + 'access-rights', 'pods-pfat', 'rest-api', ], @@ -321,6 +325,7 @@ public function provider_global_config_checks() { 'admin-ui', 'connections', 'advanced', + 'access-rights', 'pods-pfat', 'rest-api', ], @@ -349,6 +354,7 @@ public function provider_global_config_checks() { 'admin-ui', 'connections', 'advanced', + 'access-rights', 'pods-pfat', 'rest-api', ], @@ -374,6 +380,7 @@ public function provider_global_config_checks() { 'config' => [ 'pod' => [ 'groups' => [ + 'access-rights', 'pods-pfat', 'rest-api', ], @@ -399,6 +406,7 @@ public function provider_global_config_checks() { 'config' => [ 'pod' => [ 'groups' => [ + 'access-rights', 'pods-pfat', 'rest-api', ], @@ -423,6 +431,7 @@ public function provider_global_config_checks() { 'config' => [ 'pod' => [ 'groups' => [ + 'access-rights', 'rest-api', ], ], @@ -446,6 +455,7 @@ public function provider_global_config_checks() { 'config' => [ 'pod' => [ 'groups' => [ + 'access-rights', 'rest-api', ], ], @@ -469,6 +479,7 @@ public function provider_global_config_checks() { 'config' => [ 'pod' => [ 'groups' => [ + 'access-rights', 'pods-pfat', ], ], @@ -492,6 +503,7 @@ public function provider_global_config_checks() { 'config' => [ 'pod' => [ 'groups' => [ + 'access-rights', 'pods-pfat', ], ], @@ -515,6 +527,7 @@ public function provider_global_config_checks() { 'config' => [ 'pod' => [ 'groups' => [ + 'access-rights', 'pods-pfat', 'rest-api', ], @@ -539,6 +552,7 @@ public function provider_global_config_checks() { 'config' => [ 'pod' => [ 'groups' => [ + 'access-rights', 'pods-pfat', 'rest-api', ], @@ -564,6 +578,7 @@ public function provider_global_config_checks() { 'groups' => [ 'labels', 'admin-ui', + 'access-rights', ], ], 'group' => [ @@ -588,6 +603,7 @@ public function provider_global_config_checks() { 'labels', 'admin-ui', 'advanced', + 'access-rights', ], ], 'group' => [ diff --git a/tests/codeception/wpunit/Pods/PodsTest.php b/tests/codeception/wpunit/Pods/PodsTest.php index 32efce2e88..0046045b96 100644 --- a/tests/codeception/wpunit/Pods/PodsTest.php +++ b/tests/codeception/wpunit/Pods/PodsTest.php @@ -12,17 +12,91 @@ class PodsTest extends Pods_UnitTestCase { /** - * The pods system under test - * @var \Pods + * @var string */ - private $pod; + protected $pod_name = 'test_pods'; + /** + * @var int + */ + protected $pod_id = 0; + + /** + * @var Pods + */ + protected $pod; + + /** + * @var string + */ + protected $non_public_pod_name = 'test_pods_non_public'; + + /** + * @var int + */ + protected $non_public_pod_id = 0; + + /** + * @var Pods + */ + protected $non_public_pod; + + /** + * + */ public function setUp(): void { - $this->pod = pods(); + parent::setUp(); + + $api = pods_api(); + + $this->pod_id = $api->save_pod( array( + 'type' => 'pod', + 'name' => $this->pod_name, + ) ); + + $params = array( + 'pod_id' => $this->pod_id, + 'name' => 'number1', + 'type' => 'number', + ); + + $api->save_field( $params ); + + $this->pod = pods( $this->pod_name ); + + $this->non_public_pod_id = $api->save_pod( array( + 'type' => 'post_type', + 'storage' => 'meta', + 'name' => $this->non_public_pod_name, + 'public' => 0, + ) ); + + $params = array( + 'pod_id' => $this->non_public_pod_id, + 'name' => 'number2', + 'type' => 'number', + ); + + $api->save_field( $params ); + + $this->non_public_pod = pods( $this->non_public_pod_name ); } + /** + * + */ public function tearDown(): void { - unset( $this->pod ); + $this->pod_id = null; + $this->pod = null; + $this->non_public_pod_id = null; + $this->non_public_pod = null; + + pods_update_setting( 'session_auto_start', null ); + pods_update_setting( 'show_access_restricted_messages', null ); + pods_update_setting( 'show_access_admin_notices', null ); + remove_all_filters( 'pods_session_id' ); + + parent::tearDown(); } /** @@ -66,15 +140,29 @@ public function test_method_exists() { */ public function test_method_exists_valid() { $this->assertTrue( method_exists( $this->pod, 'valid' ), 'Method valid does not exist' ); + $this->assertTrue( method_exists( $this->pod, 'is_valid' ), 'Method valid does not exist' ); + } + + public function test_method_valid() { + $this->assertTrue( $this->pod->valid() ); + $this->assertTrue( $this->pod->is_valid() ); } - /** - * Test for invalid pod - * @covers Pods::valid - * @depends test_method_exists_valid - */ public function test_method_valid_invalid() { - $this->assertFalse( $this->pod->valid() ); + $this->assertFalse( pods()->valid() ); + $this->assertFalse( pods()->is_valid() ); + } + + public function test_method_valid_invalid_with_non_existent_pod() { + $pod = pods( 'truly_not_a_pod', null, false ); + + $this->assertInstanceOf( Pods::class, $pod ); + $this->assertFalse( $pod->valid() ); + $this->assertFalse( $pod->is_valid() ); + } + + public function test_method_valid_invalid_with_non_existent_pod_with_strict_mode() { + $this->assertFalse( pods( 'truly_not_a_pod', null, true ) ); } /** @@ -338,4 +426,217 @@ public function test_we_can_call_a_non_pod_taxonomy() { $this->assertTrue( $pod->valid() ); $this->assertEquals( 'taxonomy', $pod->pod_data['type'] ); } + + public function test_pods_form() { + // test shortcode + $output = $this->pod->form( [ 'check_access' => false ] ); + + $this->assertContains( 'Anonymous form submissions are not enabled for this site', $output ); + } + + public function test_pods_form_with_anon_enabled() { + pods_update_setting( 'session_auto_start', '1' ); + + // test shortcode + $output = $this->pod->form( [ 'check_access' => false ] ); + + $this->assertContains( 'Anonymous form submissions are not compatible with sessions on this site', $output ); + } + + public function test_pods_form_with_anon_enabled_and_compatible() { + pods_update_setting( 'session_auto_start', '1' ); + + add_filter( 'pods_session_id', static function() { return 'testsession'; } ); + + // test shortcode + $output = $this->pod->form( [ 'check_access' => false ] ); + + $this->assertContains( 'pod->form( [ 'check_access' => false ] ); + + $this->assertContains( ' 'testsubscriber', + 'user_email' => 'testsubscriber@test.local', + 'user_pass' => 'hayyyyyy', + 'role' => 'subscriber', + ] ); + + wp_set_current_user( $new_user_id ); + + // test shortcode + $output = $this->non_public_pod->form(); + + $this->assertContains( '$/', $Line['text'])) + { + $Block['closed'] = true; + } + + return $Block; + } + } + + protected function blockCommentContinue($Line, array $Block) + { + if (isset($Block['closed'])) + { + return; + } + + $Block['markup'] .= "\n" . $Line['body']; + + if (preg_match('/-->$/', $Line['text'])) + { + $Block['closed'] = true; + } + + return $Block; + } + + # + # Fenced Code + + protected function blockFencedCode($Line) + { + if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([^`]+)?[ ]*$/', $Line['text'], $matches)) + { + $Element = array( + 'name' => 'code', + 'text' => '', + ); + + if (isset($matches[1])) + { + /** + * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes + * Every HTML element may have a class attribute specified. + * The attribute, if specified, must have a value that is a set + * of space-separated tokens representing the various classes + * that the element belongs to. + * [...] + * The space characters, for the purposes of this specification, + * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), + * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and + * U+000D CARRIAGE RETURN (CR). + * + @license MIT + Modified by Scott Kingsley Clark on 21-February-2024 using Strauss. + @see https://github.com/BrianHenryIE/strauss +*/ + $language = substr($matches[1], 0, strcspn($matches[1], " \t\n\f\r")); + + $class = 'language-'.$language; + + $Element['attributes'] = array( + 'class' => $class, + ); + } + + $Block = array( + 'char' => $Line['text'][0], + 'element' => array( + 'name' => 'pre', + 'handler' => 'element', + 'text' => $Element, + ), + ); + + return $Block; + } + } + + protected function blockFencedCodeContinue($Line, $Block) + { + if (isset($Block['complete'])) + { + return; + } + + if (isset($Block['interrupted'])) + { + $Block['element']['text']['text'] .= "\n"; + + unset($Block['interrupted']); + } + + if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text'])) + { + $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1); + + $Block['complete'] = true; + + return $Block; + } + + $Block['element']['text']['text'] .= "\n".$Line['body']; + + return $Block; + } + + protected function blockFencedCodeComplete($Block) + { + $text = $Block['element']['text']['text']; + + $Block['element']['text']['text'] = $text; + + return $Block; + } + + # + # Header + + protected function blockHeader($Line) + { + if (isset($Line['text'][1])) + { + $level = 1; + + while (isset($Line['text'][$level]) and $Line['text'][$level] === '#') + { + $level ++; + } + + if ($level > 6) + { + return; + } + + $text = trim($Line['text'], '# '); + + $Block = array( + 'element' => array( + 'name' => 'h' . min(6, $level), + 'text' => $text, + 'handler' => 'line', + ), + ); + + return $Block; + } + } + + # + # List + + protected function blockList($Line) + { + list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]'); + + if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches)) + { + $Block = array( + 'indent' => $Line['indent'], + 'pattern' => $pattern, + 'element' => array( + 'name' => $name, + 'handler' => 'elements', + ), + ); + + if($name === 'ol') + { + $listStart = stristr($matches[0], '.', true); + + if($listStart !== '1') + { + $Block['element']['attributes'] = array('start' => $listStart); + } + } + + $Block['li'] = array( + 'name' => 'li', + 'handler' => 'li', + 'text' => array( + $matches[2], + ), + ); + + $Block['element']['text'] []= & $Block['li']; + + return $Block; + } + } + + protected function blockListContinue($Line, array $Block) + { + if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches)) + { + if (isset($Block['interrupted'])) + { + $Block['li']['text'] []= ''; + + $Block['loose'] = true; + + unset($Block['interrupted']); + } + + unset($Block['li']); + + $text = isset($matches[1]) ? $matches[1] : ''; + + $Block['li'] = array( + 'name' => 'li', + 'handler' => 'li', + 'text' => array( + $text, + ), + ); + + $Block['element']['text'] []= & $Block['li']; + + return $Block; + } + + if ($Line['text'][0] === '[' and $this->blockReference($Line)) + { + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); + + $Block['li']['text'] []= $text; + + return $Block; + } + + if ($Line['indent'] > 0) + { + $Block['li']['text'] []= ''; + + $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); + + $Block['li']['text'] []= $text; + + unset($Block['interrupted']); + + return $Block; + } + } + + protected function blockListComplete(array $Block) + { + if (isset($Block['loose'])) + { + foreach ($Block['element']['text'] as &$li) + { + if (end($li['text']) !== '') + { + $li['text'] []= ''; + } + } + } + + return $Block; + } + + # + # Quote + + protected function blockQuote($Line) + { + if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) + { + $Block = array( + 'element' => array( + 'name' => 'blockquote', + 'handler' => 'lines', + 'text' => (array) $matches[1], + ), + ); + + return $Block; + } + } + + protected function blockQuoteContinue($Line, array $Block) + { + if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) + { + if (isset($Block['interrupted'])) + { + $Block['element']['text'] []= ''; + + unset($Block['interrupted']); + } + + $Block['element']['text'] []= $matches[1]; + + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $Block['element']['text'] []= $Line['text']; + + return $Block; + } + } + + # + # Rule + + protected function blockRule($Line) + { + if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text'])) + { + $Block = array( + 'element' => array( + 'name' => 'hr' + ), + ); + + return $Block; + } + } + + # + # Setext + + protected function blockSetextHeader($Line, array $Block = null) + { + if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) + { + return; + } + + if (chop($Line['text'], $Line['text'][0]) === '') + { + $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; + + return $Block; + } + } + + # + # Markup + + protected function blockMarkup($Line) + { + if ($this->markupEscaped or $this->safeMode) + { + return; + } + + if (preg_match('/^<(\w[\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches)) + { + $element = strtolower($matches[1]); + + if (in_array($element, $this->textLevelElements)) + { + return; + } + + $Block = array( + 'name' => $matches[1], + 'depth' => 0, + 'markup' => $Line['text'], + ); + + $length = strlen($matches[0]); + + $remainder = substr($Line['text'], $length); + + if (trim($remainder) === '') + { + if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) + { + $Block['closed'] = true; + + $Block['void'] = true; + } + } + else + { + if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) + { + return; + } + + if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder)) + { + $Block['closed'] = true; + } + } + + return $Block; + } + } + + protected function blockMarkupContinue($Line, array $Block) + { + if (isset($Block['closed'])) + { + return; + } + + if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open + { + $Block['depth'] ++; + } + + if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close + { + if ($Block['depth'] > 0) + { + $Block['depth'] --; + } + else + { + $Block['closed'] = true; + } + } + + if (isset($Block['interrupted'])) + { + $Block['markup'] .= "\n"; + + unset($Block['interrupted']); + } + + $Block['markup'] .= "\n".$Line['body']; + + return $Block; + } + + # + # Reference + + protected function blockReference($Line) + { + if (preg_match('/^\[(.+?)\]:[ ]*?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches)) + { + $id = strtolower($matches[1]); + + $Data = array( + 'url' => $matches[2], + 'title' => null, + ); + + if (isset($matches[3])) + { + $Data['title'] = $matches[3]; + } + + $this->DefinitionData['Reference'][$id] = $Data; + + $Block = array( + 'hidden' => true, + ); + + return $Block; + } + } + + # + # Table + + protected function blockTable($Line, array $Block = null) + { + if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) + { + return; + } + + if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '') + { + $alignments = array(); + + $divider = $Line['text']; + + $divider = trim($divider); + $divider = trim($divider, '|'); + + $dividerCells = explode('|', $divider); + + foreach ($dividerCells as $dividerCell) + { + $dividerCell = trim($dividerCell); + + if ($dividerCell === '') + { + continue; + } + + $alignment = null; + + if ($dividerCell[0] === ':') + { + $alignment = 'left'; + } + + if (substr($dividerCell, - 1) === ':') + { + $alignment = $alignment === 'left' ? 'center' : 'right'; + } + + $alignments []= $alignment; + } + + # ~ + + $HeaderElements = array(); + + $header = $Block['element']['text']; + + $header = trim($header); + $header = trim($header, '|'); + + $headerCells = explode('|', $header); + + foreach ($headerCells as $index => $headerCell) + { + $headerCell = trim($headerCell); + + $HeaderElement = array( + 'name' => 'th', + 'text' => $headerCell, + 'handler' => 'line', + ); + + if (isset($alignments[$index])) + { + $alignment = $alignments[$index]; + + $HeaderElement['attributes'] = array( + 'style' => 'text-align: '.$alignment.';', + ); + } + + $HeaderElements []= $HeaderElement; + } + + # ~ + + $Block = array( + 'alignments' => $alignments, + 'identified' => true, + 'element' => array( + 'name' => 'table', + 'handler' => 'elements', + ), + ); + + $Block['element']['text'] []= array( + 'name' => 'thead', + 'handler' => 'elements', + ); + + $Block['element']['text'] []= array( + 'name' => 'tbody', + 'handler' => 'elements', + 'text' => array(), + ); + + $Block['element']['text'][0]['text'] []= array( + 'name' => 'tr', + 'handler' => 'elements', + 'text' => $HeaderElements, + ); + + return $Block; + } + } + + protected function blockTableContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + if ($Line['text'][0] === '|' or strpos($Line['text'], '|')) + { + $Elements = array(); + + $row = $Line['text']; + + $row = trim($row); + $row = trim($row, '|'); + + preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches); + + foreach ($matches[0] as $index => $cell) + { + $cell = trim($cell); + + $Element = array( + 'name' => 'td', + 'handler' => 'line', + 'text' => $cell, + ); + + if (isset($Block['alignments'][$index])) + { + $Element['attributes'] = array( + 'style' => 'text-align: '.$Block['alignments'][$index].';', + ); + } + + $Elements []= $Element; + } + + $Element = array( + 'name' => 'tr', + 'handler' => 'elements', + 'text' => $Elements, + ); + + $Block['element']['text'][1]['text'] []= $Element; + + return $Block; + } + } + + # + # ~ + # + + protected function paragraph($Line) + { + $Block = array( + 'element' => array( + 'name' => 'p', + 'text' => $Line['text'], + 'handler' => 'line', + ), + ); + + return $Block; + } + + # + # Inline Elements + # + + protected $InlineTypes = array( + '"' => array('SpecialCharacter'), + '!' => array('Image'), + '&' => array('SpecialCharacter'), + '*' => array('Emphasis'), + ':' => array('Url'), + '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'), + '>' => array('SpecialCharacter'), + '[' => array('Link'), + '_' => array('Emphasis'), + '`' => array('Code'), + '~' => array('Strikethrough'), + '\\' => array('EscapeSequence'), + ); + + # ~ + + protected $inlineMarkerList = '!"*_&[:<>`~\\'; + + # + # ~ + # + + public function line($text, $nonNestables=array()) + { + $markup = ''; + + # $excerpt is based on the first occurrence of a marker + + while ($excerpt = strpbrk($text, $this->inlineMarkerList)) + { + $marker = $excerpt[0]; + + $markerPosition = strpos($text, $marker); + + $Excerpt = array('text' => $excerpt, 'context' => $text); + + foreach ($this->InlineTypes[$marker] as $inlineType) + { + # check to see if the current inline type is nestable in the current context + + if ( ! empty($nonNestables) and in_array($inlineType, $nonNestables)) + { + continue; + } + + $Inline = $this->{'inline'.$inlineType}($Excerpt); + + if ( ! isset($Inline)) + { + continue; + } + + # makes sure that the inline belongs to "our" marker + + if (isset($Inline['position']) and $Inline['position'] > $markerPosition) + { + continue; + } + + # sets a default inline position + + if ( ! isset($Inline['position'])) + { + $Inline['position'] = $markerPosition; + } + + # cause the new element to 'inherit' our non nestables + + foreach ($nonNestables as $non_nestable) + { + $Inline['element']['nonNestables'][] = $non_nestable; + } + + # the text that comes before the inline + $unmarkedText = substr($text, 0, $Inline['position']); + + # compile the unmarked text + $markup .= $this->unmarkedText($unmarkedText); + + # compile the inline + $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']); + + # remove the examined text + $text = substr($text, $Inline['position'] + $Inline['extent']); + + continue 2; + } + + # the marker does not belong to an inline + + $unmarkedText = substr($text, 0, $markerPosition + 1); + + $markup .= $this->unmarkedText($unmarkedText); + + $text = substr($text, $markerPosition + 1); + } + + $markup .= $this->unmarkedText($text); + + return $markup; + } + + # + # ~ + # + + protected function inlineCode($Excerpt) + { + $marker = $Excerpt['text'][0]; + + if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(? strlen($matches[0]), + 'element' => array( + 'name' => 'code', + 'text' => $text, + ), + ); + } + } + + protected function inlineEmailTag($Excerpt) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches)) + { + $url = $matches[1]; + + if ( ! isset($matches[2])) + { + $url = 'mailto:' . $url; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $matches[1], + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + protected function inlineEmphasis($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + $marker = $Excerpt['text'][0]; + + if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) + { + $emphasis = 'strong'; + } + elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) + { + $emphasis = 'em'; + } + else + { + return; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => $emphasis, + 'handler' => 'line', + 'text' => $matches[1], + ), + ); + } + + protected function inlineEscapeSequence($Excerpt) + { + if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) + { + return array( + 'markup' => $Excerpt['text'][1], + 'extent' => 2, + ); + } + } + + protected function inlineImage($Excerpt) + { + if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') + { + return; + } + + $Excerpt['text']= substr($Excerpt['text'], 1); + + $Link = $this->inlineLink($Excerpt); + + if ($Link === null) + { + return; + } + + $Inline = array( + 'extent' => $Link['extent'] + 1, + 'element' => array( + 'name' => 'img', + 'attributes' => array( + 'src' => $Link['element']['attributes']['href'], + 'alt' => $Link['element']['text'], + ), + ), + ); + + $Inline['element']['attributes'] += $Link['element']['attributes']; + + unset($Inline['element']['attributes']['href']); + + return $Inline; + } + + protected function inlineLink($Excerpt) + { + $Element = array( + 'name' => 'a', + 'handler' => 'line', + 'nonNestables' => array('Url', 'Link'), + 'text' => null, + 'attributes' => array( + 'href' => null, + 'title' => null, + ), + ); + + $extent = 0; + + $remainder = $Excerpt['text']; + + if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) + { + $Element['text'] = $matches[1]; + + $extent += strlen($matches[0]); + + $remainder = substr($remainder, $extent); + } + else + { + return; + } + + if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*"|\'[^\']*\'))?\s*[)]/', $remainder, $matches)) + { + $Element['attributes']['href'] = $matches[1]; + + if (isset($matches[2])) + { + $Element['attributes']['title'] = substr($matches[2], 1, - 1); + } + + $extent += strlen($matches[0]); + } + else + { + if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) + { + $definition = strlen($matches[1]) ? $matches[1] : $Element['text']; + $definition = strtolower($definition); + + $extent += strlen($matches[0]); + } + else + { + $definition = strtolower($Element['text']); + } + + if ( ! isset($this->DefinitionData['Reference'][$definition])) + { + return; + } + + $Definition = $this->DefinitionData['Reference'][$definition]; + + $Element['attributes']['href'] = $Definition['url']; + $Element['attributes']['title'] = $Definition['title']; + } + + return array( + 'extent' => $extent, + 'element' => $Element, + ); + } + + protected function inlineMarkup($Excerpt) + { + if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) + { + return; + } + + if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*[ ]*>/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + } + + protected function inlineSpecialCharacter($Excerpt) + { + if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text'])) + { + return array( + 'markup' => '&', + 'extent' => 1, + ); + } + + $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot'); + + if (isset($SpecialCharacter[$Excerpt['text'][0]])) + { + return array( + 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';', + 'extent' => 1, + ); + } + } + + protected function inlineStrikethrough($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) + { + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'del', + 'text' => $matches[1], + 'handler' => 'line', + ), + ); + } + } + + protected function inlineUrl($Excerpt) + { + if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') + { + return; + } + + if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)) + { + $url = $matches[0][0]; + + $Inline = array( + 'extent' => strlen($matches[0][0]), + 'position' => $matches[0][1], + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + + return $Inline; + } + } + + protected function inlineUrlTag($Excerpt) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches)) + { + $url = $matches[1]; + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + # ~ + + protected function unmarkedText($text) + { + if ($this->breaksEnabled) + { + $text = preg_replace('/[ ]*\n/', "
    \n", $text); + } + else + { + $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "
    \n", $text); + $text = str_replace(" \n", "\n", $text); + } + + return $text; + } + + # + # Handlers + # + + protected function element(array $Element) + { + if ($this->safeMode) + { + $Element = $this->sanitiseElement($Element); + } + + $markup = '<'.$Element['name']; + + if (isset($Element['attributes'])) + { + foreach ($Element['attributes'] as $name => $value) + { + if ($value === null) + { + continue; + } + + $markup .= ' '.$name.'="'.self::escape($value).'"'; + } + } + + $permitRawHtml = false; + + if (isset($Element['text'])) + { + $text = $Element['text']; + } + // very strongly consider an alternative if you're writing an + // extension + elseif (isset($Element['rawHtml'])) + { + $text = $Element['rawHtml']; + $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode']; + $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode; + } + + if (isset($text)) + { + $markup .= '>'; + + if (!isset($Element['nonNestables'])) + { + $Element['nonNestables'] = array(); + } + + if (isset($Element['handler'])) + { + $markup .= $this->{$Element['handler']}($text, $Element['nonNestables']); + } + elseif (!$permitRawHtml) + { + $markup .= self::escape($text, true); + } + else + { + $markup .= $text; + } + + $markup .= ''; + } + else + { + $markup .= ' />'; + } + + return $markup; + } + + protected function elements(array $Elements) + { + $markup = ''; + + foreach ($Elements as $Element) + { + $markup .= "\n" . $this->element($Element); + } + + $markup .= "\n"; + + return $markup; + } + + # ~ + + protected function li($lines) + { + $markup = $this->lines($lines); + + $trimmedMarkup = trim($markup); + + if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '

    ') + { + $markup = $trimmedMarkup; + $markup = substr($markup, 3); + + $position = strpos($markup, "

    "); + + $markup = substr_replace($markup, '', $position, 4); + } + + return $markup; + } + + # + # Deprecated Methods + # + + function parse($text) + { + $markup = $this->text($text); + + return $markup; + } + + protected function sanitiseElement(array $Element) + { + static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; + static $safeUrlNameToAtt = array( + 'a' => 'href', + 'img' => 'src', + ); + + if (isset($safeUrlNameToAtt[$Element['name']])) + { + $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]); + } + + if ( ! empty($Element['attributes'])) + { + foreach ($Element['attributes'] as $att => $val) + { + # filter out badly parsed attribute + if ( ! preg_match($goodAttribute, $att)) + { + unset($Element['attributes'][$att]); + } + # dump onevent attribute + elseif (self::striAtStart($att, 'on')) + { + unset($Element['attributes'][$att]); + } + } + } + + return $Element; + } + + protected function filterUnsafeUrlInAttribute(array $Element, $attribute) + { + foreach ($this->safeLinksWhitelist as $scheme) + { + if (self::striAtStart($Element['attributes'][$attribute], $scheme)) + { + return $Element; + } + } + + $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]); + + return $Element; + } + + # + # Static Methods + # + + protected static function escape($text, $allowQuotes = false) + { + return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8'); + } + + protected static function striAtStart($string, $needle) + { + $len = strlen($needle); + + if ($len > strlen($string)) + { + return false; + } + else + { + return strtolower(substr($string, 0, $len)) === strtolower($needle); + } + } + + static function instance($name = 'default') + { + if (isset(self::$instances[$name])) + { + return self::$instances[$name]; + } + + $instance = new static(); + + self::$instances[$name] = $instance; + + return $instance; + } + + private static $instances = array(); + + # + # Fields + # + + protected $DefinitionData; + + # + # Read-Only + + protected $specialCharacters = array( + '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', + ); + + protected $StrongRegex = array( + '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s', + '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us', + ); + + protected $EmRegex = array( + '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', + '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', + ); + + protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?'; + + protected $voidElements = array( + 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', + ); + + protected $textLevelElements = array( + 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', + 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', + 'i', 'rp', 'del', 'code', 'strike', 'marquee', + 'q', 'rt', 'ins', 'font', 'strong', + 's', 'tt', 'kbd', 'mark', + 'u', 'xm', 'sub', 'nobr', + 'sup', 'ruby', + 'var', 'span', + 'wbr', 'time', + ); +} diff --git a/vendor/vendor-prefixed/erusev/parsedown/README.md b/vendor/vendor-prefixed/erusev/parsedown/README.md new file mode 100644 index 0000000000..b5d9ed2ee3 --- /dev/null +++ b/vendor/vendor-prefixed/erusev/parsedown/README.md @@ -0,0 +1,86 @@ +> I also make [Caret](https://caret.io?ref=parsedown) - a Markdown editor for Mac and PC. + +## Parsedown + +[![Build Status](https://img.shields.io/travis/erusev/parsedown/master.svg?style=flat-square)](https://travis-ci.org/erusev/parsedown) + + +Better Markdown Parser in PHP + +[Demo](http://parsedown.org/demo) | +[Benchmarks](http://parsedown.org/speed) | +[Tests](http://parsedown.org/tests/) | +[Documentation](https://github.com/erusev/parsedown/wiki/) + +### Features + +* One File +* No Dependencies +* Super Fast +* Extensible +* [GitHub flavored](https://help.github.com/articles/github-flavored-markdown) +* Tested in 5.3 to 7.1 and in HHVM +* [Markdown Extra extension](https://github.com/erusev/parsedown-extra) + +### Installation + +Include `Parsedown.php` or install [the composer package](https://packagist.org/packages/erusev/parsedown). + +### Example + +``` php +$Parsedown = new Parsedown(); + +echo $Parsedown->text('Hello _Parsedown_!'); # prints:

    Hello Parsedown!

    +``` + +More examples in [the wiki](https://github.com/erusev/parsedown/wiki/) and in [this video tutorial](http://youtu.be/wYZBY8DEikI). + +### Security + +Parsedown is capable of escaping user-input within the HTML that it generates. Additionally Parsedown will apply sanitisation to additional scripting vectors (such as scripting link destinations) that are introduced by the markdown syntax itself. + +To tell Parsedown that it is processing untrusted user-input, use the following: +```php +$parsedown = new Parsedown; +$parsedown->setSafeMode(true); +``` + +If instead, you wish to allow HTML within untrusted user-input, but still want output to be free from XSS it is recommended that you make use of a HTML sanitiser that allows HTML tags to be whitelisted, like [HTML Purifier](http://htmlpurifier.org/). + +In both cases you should strongly consider employing defence-in-depth measures, like [deploying a Content-Security-Policy](https://scotthelme.co.uk/content-security-policy-an-introduction/) (a browser security feature) so that your page is likely to be safe even if an attacker finds a vulnerability in one of the first lines of defence above. + +#### Security of Parsedown Extensions + +Safe mode does not necessarily yield safe results when using extensions to Parsedown. Extensions should be evaluated on their own to determine their specific safety against XSS. + +### Escaping HTML +> ⚠️  **WARNING:** This method isn't safe from XSS! + +If you wish to escape HTML **in trusted input**, you can use the following: +```php +$parsedown = new Parsedown; +$parsedown->setMarkupEscaped(true); +``` + +Beware that this still allows users to insert unsafe scripting vectors, such as links like `[xss](javascript:alert%281%29)`. + +### Questions + +**How does Parsedown work?** + +It tries to read Markdown like a human. First, it looks at the lines. It’s interested in how the lines start. This helps it recognise blocks. It knows, for example, that if a line starts with a `-` then perhaps it belongs to a list. Once it recognises the blocks, it continues to the content. As it reads, it watches out for special characters. This helps it recognise inline elements (or inlines). + +We call this approach "line based". We believe that Parsedown is the first Markdown parser to use it. Since the release of Parsedown, other developers have used the same approach to develop other Markdown parsers in PHP and in other languages. + +**Is it compliant with CommonMark?** + +It passes most of the CommonMark tests. Most of the tests that don't pass deal with cases that are quite uncommon. Still, as CommonMark matures, compliance should improve. + +**Who uses it?** + +[Laravel Framework](https://laravel.com/), [Bolt CMS](http://bolt.cm/), [Grav CMS](http://getgrav.org/), [Herbie CMS](http://www.getherbie.org/), [Kirby CMS](http://getkirby.com/), [October CMS](http://octobercms.com/), [Pico CMS](http://picocms.org), [Statamic CMS](http://www.statamic.com/), [phpDocumentor](http://www.phpdoc.org/), [RaspberryPi.org](http://www.raspberrypi.org/), [Symfony demo](https://github.com/symfony/symfony-demo) and [more](https://packagist.org/packages/erusev/parsedown/dependents). + +**How can I help?** + +Use it, star it, share it and if you feel generous, [donate](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=528P3NZQMP8N2). diff --git a/vendor/vendor-prefixed/erusev/parsedown/composer.json b/vendor/vendor-prefixed/erusev/parsedown/composer.json new file mode 100644 index 0000000000..f8b40f8ce8 --- /dev/null +++ b/vendor/vendor-prefixed/erusev/parsedown/composer.json @@ -0,0 +1,33 @@ +{ + "name": "erusev/parsedown", + "description": "Parser for Markdown.", + "keywords": ["markdown", "parser"], + "homepage": "http://parsedown.org", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Emanuil Rusev", + "email": "hello@erusev.com", + "homepage": "http://erusev.com" + } + ], + "require": { + "php": ">=5.3.0", + "ext-mbstring": "*" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35" + }, + "autoload": { + "psr-0": {"Parsedown": ""} + }, + "autoload-dev": { + "psr-0": { + "TestParsedown": "test/", + "ParsedownTest": "test/", + "CommonMarkTest": "test/", + "CommonMarkTestWeak": "test/" + } + } +} diff --git a/vendor/vendor-prefixed/lucatume/di52/src/App.php b/vendor/vendor-prefixed/lucatume/di52/src/App.php index a93e7fed2b..0c57ed1615 100644 --- a/vendor/vendor-prefixed/lucatume/di52/src/App.php +++ b/vendor/vendor-prefixed/lucatume/di52/src/App.php @@ -5,7 +5,7 @@ * @package lucatume\DI52 * * @license GPL-3.0 - * Modified by Scott Kingsley Clark on 24-June-2023 using Strauss. + * Modified by Scott Kingsley Clark on 21-February-2024 using Strauss. * @see https://github.com/BrianHenryIE/strauss */ @@ -93,13 +93,14 @@ public static function offsetSet($offset, $value) /** * Binds an interface a class or a string slug to an implementation and will always return the same instance. * - * @param string $id A class or interface fully qualified name or a string slug. - * @param mixed $implementation The implementation that should be bound to the alias(es); can be a - * class name, an object or a closure. - * @param array|null $afterBuildMethods An array of methods that should be called on the built - * implementation after resolving it. + * @param string|class-string $id A class or interface fully qualified name or a string slug. + * @param mixed $implementation The implementation that should be bound to the alias(es); can + * be a class name, an object or a closure. + * @param string[]|null $afterBuildMethods An array of methods that should be called on the built + * implementation after resolving it. * * @return void This method does not return any value. + * * @throws ContainerException If there's any issue reflecting on the class, interface or the implementation. */ public static function singleton($id, $implementation = null, array $afterBuildMethods = null) @@ -128,11 +129,12 @@ public static function getVar($key, $default = null) /** * Finds an entry of the container by its identifier and returns it. * - * @param string $offset Identifier of the entry to look for. + * @template T * - * @return mixed The entry for an id. + * @param string|class-string $offset Identifier of the entry to look for. * - * @return mixed The value for the offset. + * @return T|mixed The value for the offset. + * @phpstan-return ($offset is class-string ? T : mixed) * * @throws ContainerException Error while retrieving the entry. * @throws NotFoundException No entry was found for **this** identifier. @@ -145,9 +147,12 @@ public static function offsetGet($offset) /** * Finds an entry of the container by its identifier and returns it. * - * @param string $id A fully qualified class or interface name or an already built object. + * @template T + * + * @param string|class-string $id A fully qualified class or interface name or an already built object. * - * @return mixed The entry for an id. + * @return T|mixed The entry for an id. + * @phpstan-return ($id is class-string ? T : mixed) * * @throws ContainerException Error while retrieving the entry. */ @@ -163,9 +168,13 @@ public static function get($id) * If the implementation has been bound as singleton using the `singleton` method * or the ArrayAccess API then the implementation will be resolved just on the first request. * - * @param string $id A fully qualified class or interface name or an already built object. + * @template T + * + * @param string|class-string $id A fully qualified class or interface name or an already built object. + * + * @return T|mixed + * @phpstan-return ($id is class-string ? T : mixed) * - * @return mixed * @throws ContainerException If the target of the make is not bound and is not a valid, * concrete, class name or there's any issue making the target. */ @@ -181,7 +190,7 @@ public static function make($id) * `$container[$id]` returning true does not mean that `$container[$id]` will not throw an exception. * It does however mean that `$container[$id]` will not throw a `NotFoundExceptionInterface`. * - * @param string $offset An offset to check for. + * @param string|class-string $offset An offset to check for. * * @return boolean true on success or false on failure. */ @@ -197,7 +206,7 @@ public static function offsetExists($offset) * `has($id)` returning true does not mean that `get($id)` will not throw an exception. * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`. * - * @param string $id Identifier of the entry to look for. + * @param string|class-string $id Identifier of the entry to look for. * * @return bool Whether the container contains a binding for an id or not. */ @@ -275,7 +284,7 @@ public static function hasTag($tag) * If a provider overloads the `boot` method that method will be called when the `boot` method is called on the * container itself. * - * @param string $serviceProviderClass The fully-qualified Service Provider class name. + * @param class-string $serviceProviderClass The fully-qualified Service Provider class name. * @param string ...$alias A list of aliases the provider should be registered with. * @return void This method does not return any value. * @throws ContainerException If the Service Provider is not correctly configured or there's an issue @@ -296,13 +305,14 @@ public static function register($serviceProviderClass, ...$alias) * * Existing implementations are replaced. * - * @param string $id A class or interface fully qualified name or a string slug. - * @param mixed $implementation The implementation that should be bound to the alias(es); can be a - * class name, an object or a closure. - * @param array|null $afterBuildMethods An array of methods that should be called on the built - * implementation after resolving it. + * @param string|class-string $id A class or interface fully qualified name or a string slug. + * @param mixed $implementation The implementation that should be bound to the alias(es); can + * be a class name, an object or a closure. + * @param string[]|null $afterBuildMethods An array of methods that should be called on the built + * implementation after resolving it. * * @return void The method does not return any value. + * * @throws ContainerException If there's an issue while trying to bind the implementation. */ public static function bind($id, $implementation = null, array $afterBuildMethods = null) @@ -328,15 +338,14 @@ public static function boot() /** * Binds a class, interface or string slug to a chain of implementations decorating a base * object; the chain will be lazily resolved only on the first call. - * * The base decorated object must be the last element of the array. * - * @param string $id The class, interface or slug the decorator chain should - * be bound to. - * @param array $decorators An array of implementations that decorate an object. - * @param array|null $afterBuildMethods An array of methods that should be called on the - * instance after it has been built; the methods should not - * require any argument. + * @param string|class-string $id The class, interface or slug the decorator chain + * should be bound to. + * @param array $decorators An array of implementations that decorate an object. + * @param string[]|null $afterBuildMethods An array of methods that should be called on the + * instance after it has been built; the methods should + * not require any argument. * * @return void This method does not return any value. * @throws ContainerException @@ -347,17 +356,17 @@ public static function singletonDecorators($id, $decorators, array $afterBuildMe } /** - * Binds a class, interface or string slug to to a chain of implementations decorating a + * Binds a class, interface or string slug to a chain of implementations decorating a * base object. * * The base decorated object must be the last element of the array. * - * @param string $id The class, interface or slug the decorator chain should - * be bound to. - * @param array $decorators An array of implementations that decorate an object. - * @param array|null $afterBuildMethods An array of methods that should be called on the - * instance after it has been built; the methods should not - * require any argument. + * @param string|class-string $id The class, interface or slug the decorator chain + * should be bound to. + * @param array $decorators An array of implementations that decorate an object. + * @param string[]|null $afterBuildMethods An array of methods that should be called on the + * instance after it has been built; the methods should + * not require any argument. * * @return void This method does not return any value. * @throws ContainerException If there's any issue binding the decorators. @@ -382,7 +391,7 @@ public static function offsetUnset($offset) /** * Starts the `when->needs->give` chain for a contextual binding. * - * @param string $class The fully qualified name of the requesting class. + * @param string|class-string $class The fully qualified name of the requesting class. * * Example: * @@ -390,8 +399,8 @@ public static function offsetUnset($offset) * $container->singleton('LoggerInterface', 'FilesystemLogger'); * // But if the requesting class is `Worker` return another implementation * $container->when('Worker') - * ->needs('LoggerInterface) - * ->give('RemoteLogger); + * ->needs('LoggerInterface') + * ->give('RemoteLogger'); * * @return Container The container instance, to continue the when/needs/give chain. */ @@ -409,10 +418,10 @@ public static function when($class) * $container->singleton('LoggerInterface', 'FilesystemLogger'); * // But if the requesting class is `Worker` return another implementation. * $container->when('Worker') - * ->needs('LoggerInterface) - * ->give('RemoteLogger); + * ->needs('LoggerInterface') + * ->give('RemoteLogger'); * - * @param string $id The class or interface needed by the class. + * @param string|class-string $id The class or interface needed by the class. * * @return Container The container instance, to continue the when/needs/give chain. */ @@ -430,8 +439,8 @@ public static function needs($id) * $container->singleton('LoggerInterface', 'FilesystemLogger'); * // but if the requesting class is `Worker` return another implementation * $container->when('Worker') - * ->needs('LoggerInterface) - * ->give('RemoteLogger); + * ->needs('LoggerInterface') + * ->give('RemoteLogger'); * * @param mixed $implementation The implementation specified * @@ -447,13 +456,12 @@ public static function give($implementation) * Returns a lambda function suitable to use as a callback; when called the function will build the implementation * bound to `$id` and return the value of a call to `$method` method with the call arguments. * - * @param string|object $id A fully-qualified class name, a bound slug or an object o call the - * callback on. - * @param string $method The method that should be called on the resolved implementation with the - * specified array arguments. + * @param string|class-string|object $id A fully-qualified class name, a bound slug or an object o call the + * callback on. + * @param string $method The method that should be called on the resolved implementation + * with the specified array arguments. * * @return callable The callback function. - * * @throws ContainerException If the id is not a bound implementation or valid class name. */ public static function callback($id, $method) @@ -464,15 +472,14 @@ public static function callback($id, $method) /** * Returns a callable object that will build an instance of the specified class using the * specified arguments when called. - * * The callable will be a closure on PHP 5.3+ or a lambda function on PHP 5.2. * - * @param string|mixed $id The fully qualified name of a class or an interface. - * @param array $buildArgs An array of arguments that should be used to build the instance; - * note that any argument will be resolved using the container itself - * and bindings will apply. - * @param array|null $afterBuildMethods An array of methods that should be called on the built - * implementation after resolving it. + * @param string|class-string|mixed $id The fully qualified name of a class or an interface. + * @param array $buildArgs An array of arguments that should be used to build the + * instance; note that any argument will be resolved using + * the container itself and bindings will apply. + * @param string[]|null $afterBuildMethods An array of methods that should be called on the built + * implementation after resolving it. * * @return callable A callable function that will return an instance of the specified class when * called. @@ -497,7 +504,7 @@ public static function protect($value) /** * Returns the Service Provider instance registered. * - * @param string $providerId The Service Provider clas to return the instance for. + * @param string|class-string $providerId The Service Provider class to return the instance for. * * @return ServiceProvider The service provider instance. * @@ -512,10 +519,10 @@ public static function getProvider($providerId) /** * Returns whether a binding exists in the container or not. * - * `isBound($id)` returning `true` means the a call to `bind($id, $implementaion)` or `singleton($id, + * `isBound($id)` returning `true` means the call to `bind($id, $implementaion)` or `singleton($id, * $implementation)` (or equivalent ArrayAccess methods) was explicitly made. * - * @param string $id The id to check for bindings in the container. + * @param string|class-string $id The id to check for bindings in the container. * * @return bool Whether an explicit binding for the id exists in the container or not. */ diff --git a/vendor/vendor-prefixed/lucatume/di52/src/Builders/BuilderInterface.php b/vendor/vendor-prefixed/lucatume/di52/src/Builders/BuilderInterface.php index f1304ef33a..d1b4c7e6f5 100644 --- a/vendor/vendor-prefixed/lucatume/di52/src/Builders/BuilderInterface.php +++ b/vendor/vendor-prefixed/lucatume/di52/src/Builders/BuilderInterface.php @@ -5,7 +5,7 @@ * @package lucatume\DI52 * * @license GPL-3.0 - * Modified by Scott Kingsley Clark on 24-June-2023 using Strauss. + * Modified by Scott Kingsley Clark on 21-February-2024 using Strauss. * @see https://github.com/BrianHenryIE/strauss */ diff --git a/vendor/vendor-prefixed/lucatume/di52/src/Builders/CallableBuilder.php b/vendor/vendor-prefixed/lucatume/di52/src/Builders/CallableBuilder.php index 6b640b6ec3..bebc36fc89 100644 --- a/vendor/vendor-prefixed/lucatume/di52/src/Builders/CallableBuilder.php +++ b/vendor/vendor-prefixed/lucatume/di52/src/Builders/CallableBuilder.php @@ -5,7 +5,7 @@ * @package Pods\Prefixed\lucatume\DI52\Builders * * @license GPL-3.0 - * Modified by Scott Kingsley Clark on 24-June-2023 using Strauss. + * Modified by Scott Kingsley Clark on 21-February-2024 using Strauss. * @see https://github.com/BrianHenryIE/strauss */ diff --git a/vendor/vendor-prefixed/lucatume/di52/src/Builders/ClassBuilder.php b/vendor/vendor-prefixed/lucatume/di52/src/Builders/ClassBuilder.php index 9732de8b4c..ed6294afcf 100644 --- a/vendor/vendor-prefixed/lucatume/di52/src/Builders/ClassBuilder.php +++ b/vendor/vendor-prefixed/lucatume/di52/src/Builders/ClassBuilder.php @@ -5,7 +5,7 @@ * @package lucatume\DI52 * * @license GPL-3.0 - * Modified by Scott Kingsley Clark on 24-June-2023 using Strauss. + * Modified by Scott Kingsley Clark on 21-February-2024 using Strauss. * @see https://github.com/BrianHenryIE/strauss */ @@ -13,6 +13,7 @@ use Pods\Prefixed\lucatume\DI52\ContainerException; use Pods\Prefixed\lucatume\DI52\NotFoundException; +use ReflectionException; use ReflectionMethod; /** @@ -41,7 +42,7 @@ class ClassBuilder implements BuilderInterface, ReinitializableBuilderInterface /** * The fully-qualified class name the builder should build instances of. * - * @var string + * @var class-string */ protected $className; /** @@ -69,12 +70,12 @@ class ClassBuilder implements BuilderInterface, ReinitializableBuilderInterface /** * ClassBuilder constructor. * - * @param string $id The identifier associated with this builder. - * @param Resolver $resolver A reference to the resolver currently using the builder. - * @param string $className The fully-qualified class name to build instances for. - * @param array|null $afterBuildMethods An optional set of methods to call on the built object. - * @param mixed ...$buildArgs An optional set of build arguments that should be provided to the - * class constructor method. + * @param string|class-string $id The identifier associated with this builder. + * @param Resolver $resolver A reference to the resolver currently using the builder. + * @param string $className The fully-qualified class name to build instances for. + * @param array|null $afterBuildMethods An optional set of methods to call on the built object. + * @param mixed ...$buildArgs An optional set of build arguments that should be provided to + * the class constructor method. * * @throws NotFoundException If the class does not exist. */ @@ -82,7 +83,7 @@ public function __construct($id, Resolver $resolver, $className, array $afterBui { if (!class_exists($className)) { throw new NotFoundException( - "nothing is bound to the '{$className}' id and it's not an existing or instantiable class." + "nothing is bound to the '$className' id and it's not an existing or instantiable class." ); } @@ -103,6 +104,8 @@ public function __construct($id, Resolver $resolver, $className, array $afterBui * Builds and returns an instance of the class. * * @return object An instance of the class. + * + * @throws ContainerException */ public function build() { @@ -149,7 +152,8 @@ protected function resolveConstructorParameters() /** * Returns a set of resolved constructor parameters. * - * @param string $className The fully-qualified class name to get the resolved constructor parameters yet. + * @param class-string $className The fully-qualified class name to get the resolved constructor parameters yet. + * * @return array A set of resolved constructor parameters. * * @throws ContainerException If the resolution of any constructor parameters is problematic. @@ -162,7 +166,7 @@ protected function getResolvedConstructorParameters($className) try { $constructorReflection = new ReflectionMethod($className, '__construct'); - } catch (\ReflectionException $e) { + } catch (ReflectionException $e) { static::$constructorParametersCache[$className] = []; // No constructor method, no args. return []; @@ -189,6 +193,8 @@ protected function getResolvedConstructorParameters($className) * @param mixed $arg The argument id or value to resolve. * * @return mixed The resolved build argument. + * + * @throws NotFoundException */ protected function resolveBuildArg($arg) { diff --git a/vendor/vendor-prefixed/lucatume/di52/src/Builders/ClosureBuilder.php b/vendor/vendor-prefixed/lucatume/di52/src/Builders/ClosureBuilder.php index a22adf3e7d..87cd9a2889 100644 --- a/vendor/vendor-prefixed/lucatume/di52/src/Builders/ClosureBuilder.php +++ b/vendor/vendor-prefixed/lucatume/di52/src/Builders/ClosureBuilder.php @@ -5,7 +5,7 @@ * @package lucatume\DI52 * * @license GPL-3.0 - * Modified by Scott Kingsley Clark on 24-June-2023 using Strauss. + * Modified by Scott Kingsley Clark on 21-February-2024 using Strauss. * @see https://github.com/BrianHenryIE/strauss */ diff --git a/vendor/vendor-prefixed/lucatume/di52/src/Builders/Factory.php b/vendor/vendor-prefixed/lucatume/di52/src/Builders/Factory.php index 2fe8671c37..84c8d98ec6 100644 --- a/vendor/vendor-prefixed/lucatume/di52/src/Builders/Factory.php +++ b/vendor/vendor-prefixed/lucatume/di52/src/Builders/Factory.php @@ -5,7 +5,7 @@ * @package lucatume\DI52 * * @license GPL-3.0 - * Modified by Scott Kingsley Clark on 24-June-2023 using Strauss. + * Modified by Scott Kingsley Clark on 21-February-2024 using Strauss. * @see https://github.com/BrianHenryIE/strauss */ @@ -49,13 +49,12 @@ public function __construct(Container $container, Resolver $resolver) /** * Returns the correct builder for a value. * - * @param string|mixed $id The string id to provide a builder for, or a value. - * @param mixed $implementation The implementation to build the builder for. - * @param array|null $afterBuildMethods A list of methods that should be called on the built instance - * after - * it's been built. - * @param mixed ...$buildArgs A set of arguments to pass that should be used to build the - * instance, if any. + * @param string|class-string|mixed $id The string id to provide a builder for, or a value. + * @param mixed $implementation The implementation to build the builder for. + * @param string[]|null $afterBuildMethods A list of methods that should be called on the built + * instance after it's been built. + * @param mixed ...$buildArgs A set of arguments to pass that should be used to build + * the instance, if any. * * @return BuilderInterface A builder instance. * diff --git a/vendor/vendor-prefixed/lucatume/di52/src/Builders/Parameter.php b/vendor/vendor-prefixed/lucatume/di52/src/Builders/Parameter.php index cc8298cd75..74f4feb5f5 100644 --- a/vendor/vendor-prefixed/lucatume/di52/src/Builders/Parameter.php +++ b/vendor/vendor-prefixed/lucatume/di52/src/Builders/Parameter.php @@ -5,7 +5,7 @@ * @package Pods\Prefixed\lucatume\DI52\Builders * * @license GPL-3.0 - * Modified by Scott Kingsley Clark on 24-June-2023 using Strauss. + * Modified by Scott Kingsley Clark on 21-February-2024 using Strauss. * @see https://github.com/BrianHenryIE/strauss */ @@ -210,7 +210,7 @@ private function isClass() } try { - if (function_exists('enum_exists') && enum_exists($this->type)) { + if (function_exists('enum_exists') && enum_exists((string) $this->type)) { return false; } } catch (ParseError $e) { diff --git a/vendor/vendor-prefixed/lucatume/di52/src/Builders/ReinitializableBuilderInterface.php b/vendor/vendor-prefixed/lucatume/di52/src/Builders/ReinitializableBuilderInterface.php index 83ce4e5110..3fd5f2d32b 100644 --- a/vendor/vendor-prefixed/lucatume/di52/src/Builders/ReinitializableBuilderInterface.php +++ b/vendor/vendor-prefixed/lucatume/di52/src/Builders/ReinitializableBuilderInterface.php @@ -5,7 +5,7 @@ * @package Pods\Prefixed\lucatume\DI52\Builders * * @license GPL-3.0 - * Modified by Scott Kingsley Clark on 24-June-2023 using Strauss. + * Modified by Scott Kingsley Clark on 21-February-2024 using Strauss. * @see https://github.com/BrianHenryIE/strauss */ diff --git a/vendor/vendor-prefixed/lucatume/di52/src/Builders/Resolver.php b/vendor/vendor-prefixed/lucatume/di52/src/Builders/Resolver.php index 27c115dfe8..ea2d6b90f4 100644 --- a/vendor/vendor-prefixed/lucatume/di52/src/Builders/Resolver.php +++ b/vendor/vendor-prefixed/lucatume/di52/src/Builders/Resolver.php @@ -5,7 +5,7 @@ * @package Pods\Prefixed\lucatume\DI52\Builders * * @license GPL-3.0 - * Modified by Scott Kingsley Clark on 24-June-2023 using Strauss. + * Modified by Scott Kingsley Clark on 21-February-2024 using Strauss. * @see https://github.com/BrianHenryIE/strauss */ @@ -68,8 +68,8 @@ public function __construct($resolveUnboundAsSingletons = false) /** * Binds an implementation for an id, or class name, as prototype (build new each time). * - * @param string $id The id to register the implementation for. - * @param BuilderInterface $implementation The builder that will provide the implementation for the id. + * @param string|class-string $id The id to register the implementation for. + * @param BuilderInterface $implementation The builder that will provide the implementation for the id. * * @return void This method does not return any value. */ @@ -82,9 +82,9 @@ public function bind($id, BuilderInterface $implementation) /** * Registers an implementation for an id, or class name, as singleton (build at most once). * - * @param string $id The id to register the implementation for. - * @param BuilderInterface $implementation The builder that will provide the implementation for - * the id. + * @param string|class-string $id The id to register the implementation for. + * @param BuilderInterface $implementation The builder that will provide the implementation for + * the id. * * @return void This method does not return any value. */ @@ -109,7 +109,7 @@ public function isBound($id) /** * Removes the relation between an id and a bound implementation from the resolver. * - * @param string $id The id to unregister the implementation for. + * @param string|class-string $id The id to unregister the implementation for. * * @return void This method does not return any value. */ @@ -121,7 +121,7 @@ public function unbind($id) /** * Returns whether a specific id is bound as singleton (build at most once), or not. * - * @param string $id The id to check. + * @param string|class-string $id The id to check. * * @return bool Whether a specific id is bound as singleton (build at most once), or not. */ @@ -133,8 +133,8 @@ public function isSingleton($id) /** * Transform the canonical class to the class part of a when-needs-give specification, if required. * - * @param string $id The ID to resolve the when-needs-give case for. - * @param string $paramClass The class of the parameter to solve the when-needs-give case for. + * @param string|class-string $id The ID to resolve the when-needs-give case for. + * @param string $paramClass The class of the parameter to solve the when-needs-give case for. * * @return BuilderInterface|string Either the builder for the when-needs-give replacement, or the input parameter * class if not found. @@ -149,10 +149,10 @@ public function whenNeedsGive($id, $paramClass) /** * Sets an entry in the when->needs->give chain. * - * @param string $whenClass The "when" part of the chain, a class name or id. - * @param string $needsClass The "needs" part of the chain, a class name or id. - * @param BuilderInterface $builder The Builder instance that should be returned when a class needs the - * specified id. + * @param string|class-string $whenClass The "when" part of the chain, a class name or id. + * @param string|class-string $needsClass The "needs" part of the chain, a class name or id. + * @param BuilderInterface $builder The Builder instance that should be returned when a class needs the + * specified id. * * @return void This method does not return any value. */ @@ -164,11 +164,12 @@ public function setWhenNeedsGive($whenClass, $needsClass, BuilderInterface $buil /** * Resolves an ide to an implementation with the input arguments. * - * @param string|mixed $id The id, class name or built value to resolve. - * @param array|null $afterBuildMethods A list of methods that should run on the built + * @param string|class-string|mixed $id The id, class name or built value to resolve. + * @param string[]|null $afterBuildMethods A list of methods that should run on the built * instance. - * @param mixed ...$buildArgs A set of build arguments that will be passed to + * @param mixed ...$buildArgs A set of build arguments that will be passed to * the implementation constructor. + * * @return BuilderInterface|ReinitializableBuilderInterface|mixed The builder, set up to use the specified set of * build arguments. * @throws NotFoundException If the id is a string that does not resolve to an existing, concrete, class. @@ -188,11 +189,15 @@ public function resolveWithArgs($id, array $afterBuildMethods = null, ...$buildA /** * Resolves an id or input value to a value or object instance. * - * @param string|mixed $id Either the id of a bound implementation, a class name or an object - * to resolve. - * @param array|null $buildLine The build line to append the resolution leafs to, or `null` to use the - * current one. - * @return mixed The resolved value or instance. + * @template T + * + * @param string|class-string|mixed $id Either the id of a bound implementation, a class name or an + * object to resolve. + * @param string[]|null $buildLine The build line to append the resolution leafs to, or `null` to + * use the current one. + * + * @return T|mixed The resolved value or instance. + * @phpstan-return ($id is class-string ? T : mixed) * * @throws NotFoundException If the id is a string that is not bound and is not an existing, concrete, class. */ @@ -222,7 +227,7 @@ public function resolve($id, array $buildLine = null) /** * Builds, with auto-wiring, an instance of a not bound class. * - * @param string $id The class name to build an instance of. + * @param string|class-string $id The class name to build an instance of. * * @return object The built class instance. * @@ -243,7 +248,7 @@ private function resolveUnbound($id) /** * Resolves a bound implementation to a value or object. * - * @param string $id The id to resolve the implementation for. + * @param string|class-string $id The id to resolve the implementation for. * * @return mixed The resolved instance. */ @@ -259,15 +264,14 @@ private function resolveBound($id) /** * Clones the builder assigned to an id and re-initializes it. - * * The clone operation leverages the already resolved dependencies of a builder to create an up-to-date instance. * - * @param string $id The id to clone the builder of. - * @param array|null $afterBuildMethods A set of methods to run on the built instance. - * @param mixed ...$buildArgs An optional set of arguments that will be passed to the instance - * constructor. - * @return BuilderInterface A new instance of the builder currently related to the id. + * @param string|class-string $id The id to clone the builder of. + * @param string[]|null $afterBuildMethods A set of methods to run on the built instance. + * @param mixed ...$buildArgs An optional set of arguments that will be passed to the instance + * constructor. * + * @return BuilderInterface A new instance of the builder currently related to the id. * @throws NotFoundException If trying to clone the builder for a non existing id or an id that does not map to a * concrete class name. */ @@ -295,7 +299,7 @@ private function cloneBuilder($id, array $afterBuildMethods = null, ...$buildArg */ public function addToBuildLine($type, $parameterName) { - $this->buildLine[] = trim("{$type} \${$parameterName}"); + $this->buildLine[] = trim("$type \$$parameterName"); } /** @@ -304,7 +308,7 @@ public function addToBuildLine($type, $parameterName) * The build line will return a straight path from the current resolution root to the leaf * currently being resolved. Used for error logging and formatting. * - * @return array A set of consecutive items the resolver is currently trying to build. + * @return string[] A set of consecutive items the resolver is currently trying to build. */ public function getBuildLine() { diff --git a/vendor/vendor-prefixed/lucatume/di52/src/Builders/ValueBuilder.php b/vendor/vendor-prefixed/lucatume/di52/src/Builders/ValueBuilder.php index 587a9f96b5..9004ef3aab 100644 --- a/vendor/vendor-prefixed/lucatume/di52/src/Builders/ValueBuilder.php +++ b/vendor/vendor-prefixed/lucatume/di52/src/Builders/ValueBuilder.php @@ -5,7 +5,7 @@ * @package lucatume\DI52 * * @license GPL-3.0 - * Modified by Scott Kingsley Clark on 24-June-2023 using Strauss. + * Modified by Scott Kingsley Clark on 21-February-2024 using Strauss. * @see https://github.com/BrianHenryIE/strauss */ diff --git a/vendor/vendor-prefixed/lucatume/di52/src/Container.php b/vendor/vendor-prefixed/lucatume/di52/src/Container.php index a5ea04f022..235854b64b 100644 --- a/vendor/vendor-prefixed/lucatume/di52/src/Container.php +++ b/vendor/vendor-prefixed/lucatume/di52/src/Container.php @@ -5,7 +5,7 @@ * @package lucatume\DI52 * * @license GPL-3.0 - * Modified by Scott Kingsley Clark on 24-June-2023 using Strauss. + * Modified by Scott Kingsley Clark on 21-February-2024 using Strauss. * @see https://github.com/BrianHenryIE/strauss */ @@ -51,7 +51,7 @@ class Container implements ArrayAccess, ContainerInterface /** * A list of bound and resolved singletons. * - * @var array + * @var array */ protected $singletons = []; /** @@ -186,11 +186,12 @@ public function getVar($key, $default = null) /** * Finds an entry of the container by its identifier and returns it. * - * @param string $offset Identifier of the entry to look for. + * @template T * - * @return mixed The entry for an id. + * @param string|class-string $offset Identifier of the entry to look for. * - * @return mixed The value for the offset. + * @return T|mixed The value for the offset. + * @phpstan-return ($offset is class-string ? T : mixed) * * @throws ContainerException Error while retrieving the entry. * @throws NotFoundException No entry was found for **this** identifier. @@ -204,9 +205,12 @@ public function offsetGet($offset) /** * Finds an entry of the container by its identifier and returns it. * - * @param string $id A fully qualified class or interface name or an already built object. + * @template T + * + * @param string|class-string $id A fully qualified class or interface name or an already built object. * - * @return mixed The entry for an id. + * @return T|mixed The entry for an id. + * @phpstan-return ($id is class-string ? T : mixed) * * @throws ContainerException Error while retrieving the entry. */ @@ -252,9 +256,13 @@ private function castThrown($thrown, $id) * If the implementation has been bound as singleton using the `singleton` method * or the ArrayAccess API then the implementation will be resolved just on the first request. * - * @param string $id A fully qualified class or interface name or an already built object. + * @template T + * + * @param string|class-string $id A fully qualified class or interface name or an already built object. + * + * @return T|mixed + * @phpstan-return ($id is class-string ? T : mixed) * - * @return mixed * @throws ContainerException If the target of the make is not bound and is not a valid, * concrete, class name or there's any issue making the target. */ @@ -270,7 +278,7 @@ public function make($id) * `$container[$id]` returning true does not mean that `$container[$id]` will not throw an exception. * It does however mean that `$container[$id]` will not throw a `NotFoundExceptionInterface`. * - * @param string $offset An offset to check for. + * @param string|class-string $offset An offset to check for. * * @return boolean true on success or false on failure. */ @@ -287,7 +295,7 @@ public function offsetExists($offset) * `has($id)` returning true does not mean that `get($id)` will not throw an exception. * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`. * - * @param string $id Identifier of the entry to look for. + * @param string|class-string $id Identifier of the entry to look for. * * @return bool Whether the container contains a binding for an id or not. */ @@ -367,7 +375,7 @@ public function hasTag($tag) /** * A wrapper around the `class_exists` function to capture and handle possible fatal errors on PHP 7.0+. * - * @param string $class The class name to check. + * @param string|class-string $class The class name to check. * * @return bool Whether the class exists or not. * @@ -401,7 +409,7 @@ protected function classIsInstantiable($class) /** * Checks a class, interface or trait exists. * - * @param string $class The class, interface or trait to check. + * @param string|class-string $class The class, interface or trait to check. * * @return bool Whether the class, interface or trait exists or not. * @throws ReflectionException If the class should be checked for concreteness and it does not exist. @@ -436,7 +444,7 @@ protected function checkClassIsInstantiatable($class) * If a provider overloads the `boot` method that method will be called when the `boot` method is called on the * container itself. * - * @param string $serviceProviderClass The fully-qualified Service Provider class name. + * @param class-string $serviceProviderClass The fully-qualified Service Provider class name. * @param string ...$alias A list of aliases the provider should be registered with. * @return void This method does not return any value. * @throws ContainerException If the Service Provider is not correctly configured or there's an issue @@ -512,13 +520,14 @@ private function getDeferredProviderMakeClosure(ServiceProvider $provider, $id) * * Existing implementations are replaced. * - * @param string $id A class or interface fully qualified name or a string slug. - * @param mixed $implementation The implementation that should be bound to the alias(es); can be a - * class name, an object or a closure. - * @param array|null $afterBuildMethods An array of methods that should be called on the built - * implementation after resolving it. + * @param string|class-string $id A class or interface fully qualified name or a string slug. + * @param mixed $implementation The implementation that should be bound to the alias(es); can + * be a class name, an object or a closure. + * @param string[]|null $afterBuildMethods An array of methods that should be called on the built + * implementation after resolving it. * * @return void The method does not return any value. + * * @throws ContainerException If there's an issue while trying to bind the implementation. */ public function bind($id, $implementation = null, array $afterBuildMethods = null) @@ -555,15 +564,14 @@ public function boot() /** * Binds a class, interface or string slug to a chain of implementations decorating a base * object; the chain will be lazily resolved only on the first call. - * * The base decorated object must be the last element of the array. * - * @param string $id The class, interface or slug the decorator chain should - * be bound to. - * @param array $decorators An array of implementations that decorate an object. - * @param array|null $afterBuildMethods An array of methods that should be called on the - * instance after it has been built; the methods should not - * require any argument. + * @param string|class-string $id The class, interface or slug the decorator chain + * should be bound to. + * @param array $decorators An array of implementations that decorate an object. + * @param string[]|null $afterBuildMethods An array of methods that should be called on the + * instance after it has been built; the methods should + * not require any argument. * * @return void This method does not return any value. * @throws ContainerException @@ -603,17 +611,17 @@ private function getDecoratorBuilder(array $decorators, $id, array $afterBuildMe } /** - * Binds a class, interface or string slug to to a chain of implementations decorating a + * Binds a class, interface or string slug to a chain of implementations decorating a * base object. * * The base decorated object must be the last element of the array. * - * @param string $id The class, interface or slug the decorator chain should - * be bound to. - * @param array $decorators An array of implementations that decorate an object. - * @param array|null $afterBuildMethods An array of methods that should be called on the - * instance after it has been built; the methods should not - * require any argument. + * @param string|class-string $id The class, interface or slug the decorator chain + * should be bound to. + * @param array $decorators An array of implementations that decorate an object. + * @param string[]|null $afterBuildMethods An array of methods that should be called on the + * instance after it has been built; the methods should + * not require any argument. * * @return void This method does not return any value. * @throws ContainerException If there's any issue binding the decorators. @@ -643,7 +651,7 @@ public function offsetUnset($offset) /** * Starts the `when->needs->give` chain for a contextual binding. * - * @param string $class The fully qualified name of the requesting class. + * @param string|class-string $class The fully qualified name of the requesting class. * * Example: * @@ -651,8 +659,8 @@ public function offsetUnset($offset) * $container->singleton('LoggerInterface', 'FilesystemLogger'); * // But if the requesting class is `Worker` return another implementation * $container->when('Worker') - * ->needs('LoggerInterface) - * ->give('RemoteLogger); + * ->needs('LoggerInterface') + * ->give('RemoteLogger'); * * @return Container The container instance, to continue the when/needs/give chain. */ @@ -672,10 +680,10 @@ public function when($class) * $container->singleton('LoggerInterface', 'FilesystemLogger'); * // But if the requesting class is `Worker` return another implementation. * $container->when('Worker') - * ->needs('LoggerInterface) - * ->give('RemoteLogger); + * ->needs('LoggerInterface') + * ->give('RemoteLogger'); * - * @param string $id The class or interface needed by the class. + * @param string|class-string $id The class or interface needed by the class. * * @return Container The container instance, to continue the when/needs/give chain. */ @@ -695,8 +703,8 @@ public function needs($id) * $container->singleton('LoggerInterface', 'FilesystemLogger'); * // but if the requesting class is `Worker` return another implementation * $container->when('Worker') - * ->needs('LoggerInterface) - * ->give('RemoteLogger); + * ->needs('LoggerInterface') + * ->give('RemoteLogger'); * * @param mixed $implementation The implementation specified * @@ -715,13 +723,12 @@ public function give($implementation) * Returns a lambda function suitable to use as a callback; when called the function will build the implementation * bound to `$id` and return the value of a call to `$method` method with the call arguments. * - * @param string|object $id A fully-qualified class name, a bound slug or an object o call the - * callback on. - * @param string $method The method that should be called on the resolved implementation with the - * specified array arguments. - * - * @return callable The callback function. + * @param string|class-string|object $id A fully-qualified class name, a bound slug or an object o call the + * callback on. + * @param string $method The method that should be called on the resolved implementation + * with the specified array arguments. * + * @return callable|Closure The callback function. * @throws ContainerException If the id is not a bound implementation or valid class name. */ public function callback($id, $method) @@ -763,8 +770,8 @@ public function callback($id, $method) /** * Whether a method of an id, possibly not a class, is static or not. * - * @param object|string $object A class name, instance or something that does not map to a class. - * @param string $method The method to check. + * @param object|string|class-string $object A class name, instance or something that does not map to a class. + * @param string $method The method to check. * * @return bool Whether a method of an id or class is static or not. */ @@ -786,17 +793,16 @@ protected function isStaticMethod($object, $method) /** * Returns a callable object that will build an instance of the specified class using the * specified arguments when called. - * * The callable will be a closure on PHP 5.3+ or a lambda function on PHP 5.2. * - * @param string|mixed $id The fully qualified name of a class or an interface. - * @param array $buildArgs An array of arguments that should be used to build the instance; - * note that any argument will be resolved using the container itself - * and bindings will apply. - * @param array|null $afterBuildMethods An array of methods that should be called on the built - * implementation after resolving it. + * @param string|class-string|mixed $id The fully qualified name of a class or an interface. + * @param array $buildArgs An array of arguments that should be used to build the + * instance; note that any argument will be resolved using + * the container itself and bindings will apply. + * @param string[]|null $afterBuildMethods An array of methods that should be called on the built + * implementation after resolving it. * - * @return callable A callable function that will return an instance of the specified class when + * @return callable|Closure A callable function that will return an instance of the specified class when * called. */ public function instance($id, array $buildArgs = [], array $afterBuildMethods = null) @@ -825,7 +831,7 @@ public function protect($value) /** * Returns the Service Provider instance registered. * - * @param string $providerId The Service Provider clas to return the instance for. + * @param string|class-string $providerId The Service Provider clas to return the instance for. * * @return ServiceProvider The service provider instance. * @@ -835,13 +841,13 @@ public function protect($value) public function getProvider($providerId) { if (!$this->resolver->isBound($providerId)) { - throw new NotFoundException("Service provider '{$providerId}' is not registered in the container."); + throw new NotFoundException("Service provider '$providerId' is not registered in the container."); } $provider = $this->get($providerId); if (! $provider instanceof ServiceProvider) { - throw new NotFoundException("Bound implementation for '{$providerId}' is not Service Provider."); + throw new NotFoundException("Bound implementation for '$providerId' is not Service Provider."); } return $provider; diff --git a/vendor/vendor-prefixed/lucatume/di52/src/ContainerException.php b/vendor/vendor-prefixed/lucatume/di52/src/ContainerException.php index bd3c48cea8..56aac109c1 100644 --- a/vendor/vendor-prefixed/lucatume/di52/src/ContainerException.php +++ b/vendor/vendor-prefixed/lucatume/di52/src/ContainerException.php @@ -5,7 +5,7 @@ * @package lucatume\DI52 * * @license GPL-3.0 - * Modified by Scott Kingsley Clark on 24-June-2023 using Strauss. + * Modified by Scott Kingsley Clark on 21-February-2024 using Strauss. * @see https://github.com/BrianHenryIE/strauss */ diff --git a/vendor/vendor-prefixed/lucatume/di52/src/NestedParseError.php b/vendor/vendor-prefixed/lucatume/di52/src/NestedParseError.php index 7915611a29..0638c8f8bc 100644 --- a/vendor/vendor-prefixed/lucatume/di52/src/NestedParseError.php +++ b/vendor/vendor-prefixed/lucatume/di52/src/NestedParseError.php @@ -6,7 +6,7 @@ * @package Pods\Prefixed\lucatume\DI52; * * @license GPL-3.0 - * Modified by Scott Kingsley Clark on 24-June-2023 using Strauss. + * Modified by Scott Kingsley Clark on 21-February-2024 using Strauss. * @see https://github.com/BrianHenryIE/strauss */ diff --git a/vendor/vendor-prefixed/lucatume/di52/src/NotFoundException.php b/vendor/vendor-prefixed/lucatume/di52/src/NotFoundException.php index d376e826a7..3841d84716 100644 --- a/vendor/vendor-prefixed/lucatume/di52/src/NotFoundException.php +++ b/vendor/vendor-prefixed/lucatume/di52/src/NotFoundException.php @@ -5,7 +5,7 @@ * @package lucatume\DI52 * * @license GPL-3.0 - * Modified by Scott Kingsley Clark on 24-June-2023 using Strauss. + * Modified by Scott Kingsley Clark on 21-February-2024 using Strauss. * @see https://github.com/BrianHenryIE/strauss */ diff --git a/vendor/vendor-prefixed/lucatume/di52/src/ServiceProvider.php b/vendor/vendor-prefixed/lucatume/di52/src/ServiceProvider.php index 3ef63fe1d6..d756550eb2 100644 --- a/vendor/vendor-prefixed/lucatume/di52/src/ServiceProvider.php +++ b/vendor/vendor-prefixed/lucatume/di52/src/ServiceProvider.php @@ -3,7 +3,7 @@ * The base service provider class. * * @license GPL-3.0 - * Modified by Scott Kingsley Clark on 24-June-2023 using Strauss. + * Modified by Scott Kingsley Clark on 21-February-2024 using Strauss. * @see https://github.com/BrianHenryIE/strauss */ diff --git a/vendor/vendor-prefixed/mustangostang/spyc/Spyc.php b/vendor/vendor-prefixed/mustangostang/spyc/Spyc.php index 67c593a539..b22d96c080 100644 --- a/vendor/vendor-prefixed/mustangostang/spyc/Spyc.php +++ b/vendor/vendor-prefixed/mustangostang/spyc/Spyc.php @@ -9,7 +9,7 @@ * @license http://www.opensource.org/licenses/mit-license.php MIT License * @package Pods__Prefixed__Spyc * - * Modified by Scott Kingsley Clark on 24-June-2023 using Strauss. + * Modified by Scott Kingsley Clark on 21-February-2024 using Strauss. * @see https://github.com/BrianHenryIE/strauss */ diff --git a/vendor/vendor-prefixed/psr/container/src/ContainerExceptionInterface.php b/vendor/vendor-prefixed/psr/container/src/ContainerExceptionInterface.php index d7055da352..1b421e9686 100644 --- a/vendor/vendor-prefixed/psr/container/src/ContainerExceptionInterface.php +++ b/vendor/vendor-prefixed/psr/container/src/ContainerExceptionInterface.php @@ -2,7 +2,7 @@ /** * @license MIT * - * Modified by Scott Kingsley Clark on 24-June-2023 using Strauss. + * Modified by Scott Kingsley Clark on 21-February-2024 using Strauss. * @see https://github.com/BrianHenryIE/strauss */ diff --git a/vendor/vendor-prefixed/psr/container/src/ContainerInterface.php b/vendor/vendor-prefixed/psr/container/src/ContainerInterface.php index da0afb91fa..593ed77762 100644 --- a/vendor/vendor-prefixed/psr/container/src/ContainerInterface.php +++ b/vendor/vendor-prefixed/psr/container/src/ContainerInterface.php @@ -2,7 +2,7 @@ /** * @license MIT * - * Modified by Scott Kingsley Clark on 24-June-2023 using Strauss. + * Modified by Scott Kingsley Clark on 21-February-2024 using Strauss. * @see https://github.com/BrianHenryIE/strauss */ diff --git a/vendor/vendor-prefixed/psr/container/src/NotFoundExceptionInterface.php b/vendor/vendor-prefixed/psr/container/src/NotFoundExceptionInterface.php index d16e000b61..1d710d2466 100644 --- a/vendor/vendor-prefixed/psr/container/src/NotFoundExceptionInterface.php +++ b/vendor/vendor-prefixed/psr/container/src/NotFoundExceptionInterface.php @@ -2,7 +2,7 @@ /** * @license MIT * - * Modified by Scott Kingsley Clark on 24-June-2023 using Strauss. + * Modified by Scott Kingsley Clark on 21-February-2024 using Strauss. * @see https://github.com/BrianHenryIE/strauss */