From 6fd0f2db4ecbfc1577fdfec6c184b377e4aa372a Mon Sep 17 00:00:00 2001 From: Scott Kingsley Clark Date: Wed, 7 Feb 2024 12:05:16 -0600 Subject: [PATCH] Pods 2.8.23.1 --- classes/Pods.php | 232 ++- classes/PodsAPI.php | 51 +- classes/PodsAdmin.php | 40 +- classes/PodsData.php | 25 +- classes/PodsField.php | 44 +- classes/PodsInit.php | 25 +- classes/PodsUI.php | 38 +- classes/PodsView.php | 71 +- classes/fields/code.php | 3 +- classes/fields/datetime.php | 5 +- classes/fields/number.php | 5 +- classes/fields/oembed.php | 2 +- components/Templates/Templates.php | 55 +- .../includes/functions-view_template.php | 11 +- includes/access.php | 1708 +++++++++++++++++ includes/classes.php | 5 +- includes/data.php | 111 +- includes/general.php | 435 ++++- init.php | 11 +- package.json | 2 +- readme.txt | 21 +- sql/update-1.x.php | 2 +- sql/upgrade/PodsUpgrade_2_0_0.php | 2 +- src/Pods/Blocks/API.php | 22 +- src/Pods/Blocks/Types/Base.php | 34 + src/Pods/Blocks/Types/Field.php | 14 +- src/Pods/Blocks/Types/Item_List.php | 14 +- src/Pods/Blocks/Types/Item_Single.php | 12 +- src/Pods/Data/Map_Field_Values.php | 4 - src/Pods/Permissions.php | 4 +- src/Pods/Whatsit/Storage/Post_Type.php | 2 +- tests/codeception/wpunit/Pods/PodsTest.php | 193 +- .../wpunit/Pods/Shortcode/EachTest.php | 30 +- .../wpunit/Pods/Shortcode/IfTest.php | 22 +- .../wpunit/Pods/Shortcode/PodsTest.php | 179 +- tests/codeception/wpunit/Pods/WhatsitTest.php | 2 +- ui/admin/components-admin.php | 6 + ui/admin/help.php | 6 + ui/admin/postbox-header.php | 5 + ui/admin/settings-reset.php | 5 + ui/admin/settings-settings.php | 11 +- ui/admin/settings.php | 6 + ui/admin/setup-add.php | 5 + ui/admin/setup-edit.php | 5 + ui/admin/shortcode.php | 6 + ui/admin/upgrade/backup.php | 6 + ui/admin/upgrade/upgrade_2_0_0.php | 5 + ui/admin/upgrade/upgrade_2_1_0.php | 5 + ui/admin/view.php | 5 + ui/admin/widgets/field.php | 6 + ui/admin/widgets/form.php | 6 + ui/admin/widgets/list.php | 6 + ui/admin/widgets/single.php | 6 + ui/admin/widgets/view.php | 6 + ui/fields/_comment.php | 6 + ui/fields/_db.php | 5 + ui/fields/_hidden.php | 5 + ui/fields/_label.php | 6 + ui/fields/_row.php | 13 +- ui/fields/attachment.php | 5 + ui/fields/checkbox.php | 5 + ui/fields/cleditor.php | 5 + ui/fields/codemirror.php | 5 + ui/fields/color.php | 5 + ui/fields/currency.php | 5 + ui/fields/date.php | 5 + ui/fields/datetime.php | 5 + ui/fields/email.php | 5 + ui/fields/link.php | 4 + ui/fields/number.php | 5 + ui/fields/oembed.php | 5 + ui/fields/password.php | 5 + ui/fields/phone.php | 5 + ui/fields/radio.php | 5 + ui/fields/select.php | 5 + ui/fields/slider.php | 5 + ui/fields/slug.php | 5 + ui/fields/text.php | 7 +- ui/fields/textarea.php | 5 + ui/fields/time.php | 5 + ui/fields/tinymce.php | 5 + ui/fields/website.php | 5 + ui/forms/div-row.php | 5 + ui/forms/div-rows.php | 5 + ui/forms/form.php | 5 + ui/forms/list-row.php | 5 + ui/forms/list-rows.php | 5 + ui/forms/p-row.php | 5 + ui/forms/p-rows.php | 5 + ui/forms/table-row.php | 5 + ui/forms/table-rows.php | 5 + ui/forms/type/post.php | 5 + ui/forms/type/settings.php | 5 + ui/front/filters.php | 6 + ui/front/form.php | 5 + ui/front/pagination/advanced.php | 6 + ui/front/pagination/list.php | 6 + ui/front/pagination/paginate.php | 6 + ui/front/pagination/simple.php | 6 + ui/front/view.php | 5 + ui/front/widgets.php | 5 + 101 files changed, 3458 insertions(+), 335 deletions(-) create mode 100644 includes/access.php diff --git a/classes/Pods.php b/classes/Pods.php index edaab48f02..de0a799cc4 100644 --- a/classes/Pods.php +++ b/classes/Pods.php @@ -1315,10 +1315,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; @@ -1331,6 +1329,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 ) { @@ -1349,7 +1351,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 ); @@ -3462,20 +3464,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. * @@ -3486,78 +3474,42 @@ 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 = array( - 'system', - 'exec', - 'popen', - 'eval', - 'preg_replace', - 'preg_replace_array', - 'preg_replace_callback', - 'preg_replace_callback_array', - 'preg_match', - 'preg_match_all', - 'create_function', - 'include', - 'include_once', - 'require', - 'require_once', - ); - - $allowed = array(); - - /** - * 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 ); } - if ( $include_obj ) { - return call_user_func( $params['helper'], $value, $this ); + try { + if ( $include_obj ) { + return call_user_func( $params['helper'], $value, $this ); + } + + return call_user_func( $params['helper'], $value ); + } catch ( Exception $exception ) { + pods_debug_log( $exception ); + + if ( pods_is_debug_display() ) { + throw $exception; + } } - return call_user_func( $params['helper'], $value ); + return ''; } /** @@ -3596,20 +3548,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 ); @@ -3632,18 +3585,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 ); + } } } @@ -3696,10 +3674,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(); @@ -3768,7 +3769,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' ) ); } @@ -3793,13 +3794,14 @@ 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' => false, + ]; if ( is_array( $params ) ) { $params = array_merge( $defaults, $params ); @@ -3807,6 +3809,50 @@ 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 + ) + && ! $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 ); @@ -3943,7 +3989,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 4c64eb4409..fea7729e60 100644 --- a/classes/PodsAPI.php +++ b/classes/PodsAPI.php @@ -939,6 +939,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', @@ -10040,7 +10041,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 ); @@ -10816,9 +10817,17 @@ public function process_form( $params, $obj = null, $fields = null, $thank_you = $form = pods_v_sanitized( '_pods_form', $params ); $location = pods_v_sanitized( '_pods_location', $params ); + $obj = null; + if ( is_object( $obj ) ) { $pod = $obj->pod; $id = $obj->id(); + } elseif ( $pod ) { + $obj = pods( $pod, $id, true ); + + if ( ! $obj || is_wp_error( $obj ) || ! $obj->is_valid() ) { + $obj = null; + } } if ( ! empty( $fields ) ) { @@ -10856,6 +10865,46 @@ public function process_form( $params, $obj = null, $fields = null, $thank_you = $data[ $field ] = pods_v( 'pods_field_' . $field, $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). + */ + $restrict_content = (bool) apply_filters( + 'pods_api_process_form_restrict_content', + $restrict_content, + $obj->pod_data->get_type(), + $obj->pod_data->get_name(), + $id + ); + + 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 af001ccb1c..4596c796bb 100644 --- a/classes/PodsAdmin.php +++ b/classes/PodsAdmin.php @@ -2079,6 +2079,28 @@ public function admin_advanced() { * Get settings administration view */ public function admin_settings() { + $hide_notice = filter_var( get_option( 'pods_tmp_hide_notice_31', false ), FILTER_VALIDATE_BOOLEAN ); + + if ( ! $hide_notice ) { + if ( + ! empty( $_GET['hide_notice_31'] ) + && ! empty( $_GET['hide_notice_31_nonce'] ) + && wp_verify_nonce( $_GET['hide_notice_31_nonce'], 'hide_notice_31' ) + ) { + update_option( 'pods_tmp_hide_notice_31', 1, 'no' ); + } else { + pods_message( + sprintf( + '⚠️ %s %s', + __( 'You are running an outdated version of Pods. Upgrade to Pods 3.1 to get the most out of Access Rights, security fixes, and future features.', 'pods' ), + esc_url( add_query_arg( [ 'hide_notice_31' => 1, 'hide_notice_31_nonce' => wp_create_nonce( 'hide_notice_31' ) ] ) ), + __( 'Hide this notice', 'pods' ) + ), + 'error' + ); + } + } + /** * Allow hooking into our settings page to set up hooks. * @@ -2642,7 +2664,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 ); } @@ -2655,8 +2684,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; diff --git a/classes/PodsData.php b/classes/PodsData.php index 719e6d9e5a..c81e6557e2 100644 --- a/classes/PodsData.php +++ b/classes/PodsData.php @@ -712,6 +712,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; @@ -2112,6 +2131,8 @@ public function fetch( $row = null, $explicit_set = true ) { $this->row = false; } else { $current_row_id = $this->row['ID']; + + $this->row = pods_access_bleep_data( $this->row ); } $get_table_data = true; @@ -2188,7 +2209,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 = $this->row['ID']; } @@ -2292,6 +2313,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 d897c113f9..6dc5a502d9 100644 --- a/classes/PodsField.php +++ b/classes/PodsField.php @@ -286,9 +286,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 ); } /** @@ -899,11 +897,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 80997e09bf..33de73a6e2 100644 --- a/classes/PodsInit.php +++ b/classes/PodsInit.php @@ -1674,6 +1674,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 ) ), @@ -2750,14 +2751,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/PodsUI.php b/classes/PodsUI.php index 209fc2fec1..803ce7ac71 100644 --- a/classes/PodsUI.php +++ b/classes/PodsUI.php @@ -1296,7 +1296,7 @@ public function go() { $this->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'] ) { @@ -1317,10 +1317,16 @@ public function go() { $this->view(); } else { if ( isset( $this->actions_custom[ $this->action ] ) ) { - $more_args = false; + $use_nonce = false; - if ( is_array( $this->actions_custom[ $this->action ] ) && isset( $this->actions_custom[ $this->action ]['more_args'] ) ) { - $more_args = $this->actions_custom[ $this->action ]['more_args']; + if ( is_array( $this->actions_custom[ $this->action ] ) ) { + $more_args = []; + + if ( ! empty( $this->actions_custom[ $this->action ]['more_args'] ) ) { + $more_args = $this->actions_custom[ $this->action ]['more_args']; + } + + $use_nonce = ! empty( $this->actions_custom[ $this->action ]['nonce'] ) || ! empty( $more_args['nonce'] ); } $row = $this->row; @@ -1329,7 +1335,13 @@ public function go() { $row = $this->get_row(); } - if ( $this->restricted( $this->action, $row ) || ( $more_args && ! empty( $more_args['nonce'] ) && false === wp_verify_nonce( $this->_nonce, 'pods-ui-action-' . $this->action ) ) ) { + if ( + $this->restricted( $this->action, $row ) + || ( + $use_nonce + && 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 ) ); } elseif ( $more_args && false !== $this->callback_action( true, $this->action, $this->id, $row ) ) { return null; @@ -1542,7 +1554,7 @@ public function form( $create = false, $duplicate = false ) { } $label = $this->do_template( $this->label['edit'] ); - $id = $this->row[ $this->sql['field_id'] ]; + $id = pods_v( $this->sql['field_id'], $this->row ); $vars = array( $this->num_prefix . 'action' . $this->num => $this->action_after['edit'], $this->num_prefix . 'do' . $this->num => 'save', @@ -3176,15 +3188,15 @@ public function filters() { ); if ( $this->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 ); } ?> -
  • +
  • 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' ) . ''; @@ -4460,7 +4472,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 ) { @@ -4471,7 +4483,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 3ad05fed78..2375f3b7b3 100644 --- a/classes/PodsView.php +++ b/classes/PodsView.php @@ -27,12 +27,13 @@ private function __construct() { * @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. @@ -44,6 +45,7 @@ public static function view( $view, $data = null, $expires = false, $cache_mode * @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 * * @since 2.4.1 */ @@ -84,7 +86,7 @@ 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 ); + $view = apply_filters( 'pods_view_inc', $view, $data, $expires, $cache_mode, $limited ); $view_key = $view; @@ -128,6 +130,53 @@ public static function view( $view, $data = null, $expires = false, $cache_mode 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 * @@ -507,10 +556,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. @@ -527,7 +577,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; @@ -547,11 +597,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 = []; @@ -580,9 +631,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 66d5370420..b030b17484 100644 --- a/classes/fields/code.php +++ b/classes/fields/code.php @@ -100,12 +100,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 5cbaa619fe..c544974e1d 100644 --- a/classes/fields/datetime.php +++ b/classes/fields/datetime.php @@ -275,10 +275,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 3911244c5d..a5581ccc2a 100644 --- a/classes/fields/number.php +++ b/classes/fields/number.php @@ -200,10 +200,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 b623c8baa6..5b718dcbbf 100644 --- a/classes/fields/oembed.php +++ b/classes/fields/oembed.php @@ -284,7 +284,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 ); diff --git a/components/Templates/Templates.php b/components/Templates/Templates.php index db76f9db12..8201944490 100644 --- a/components/Templates/Templates.php +++ b/components/Templates/Templates.php @@ -498,11 +498,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; @@ -578,16 +579,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( @@ -600,10 +626,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 7e54ace250..f3760d9ed8 100644 --- a/components/Templates/includes/functions-view_template.php +++ b/components/Templates/includes/functions-view_template.php @@ -13,7 +13,6 @@ add_filter( 'pods_templates_do_template', 'frontier_do_shortcode', 25, 1 ); // template shortcode handlers -add_shortcode( 'pod_sub_template', 'frontier_do_subtemplate' ); add_shortcode( 'pod_once_template', 'frontier_template_once_blocks' ); add_shortcode( 'pod_after_template', 'frontier_template_blocks' ); add_shortcode( 'pod_before_template', 'frontier_template_blocks' ); @@ -474,12 +473,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 @@ -654,6 +655,10 @@ function frontier_prefilter_template( $code, $template, $pod ) { 'if' => 'pod_if_field', ); + if ( ! shortcode_exists( 'pod_sub_template' ) ) { + add_shortcode( 'pod_sub_template', 'frontier_do_subtemplate' ); + } + $commands = array_merge( $commands, get_option( 'pods_frontier_extra_commands', array() ) ); /** diff --git a/includes/access.php b/includes/access.php new file mode 100644 index 0000000000..8b1a391b74 --- /dev/null +++ b/includes/access.php @@ -0,0 +1,1708 @@ + 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( $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, $user_id, $access_type = 'edit', $context = null ) { + $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_admin() ) { + 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, $access_type = 'edit', $context = null ) { + $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, $user_id = null, $strict = false ) { + $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, $context = 'shortcode' ) { + $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 ) { + $is_public = false; + } else { + $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 ) { + $is_public = false; + } else { + $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 ) { + $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 ) { + $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; + } + + $status_obj = get_post_status_object( $post->post_status ); + + $bypass_private_post = false; + + if ( + ! is_object( $status_obj ) || + ! empty( $status_obj->internal ) || + ! empty( $status_obj->protected ) + ) { + $is_public = false; + } else { + $is_public = ! empty( $status_obj->publicly_queryable ) || ( ! empty( $status_obj->_builtin ) && ! empty( $status_obj->public ) ); + } + + if ( ! $is_public ) { + $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 = null ) { + if ( defined( 'PODS_DYNAMIC_FEATURES_ALLOW' ) ) { + return PODS_DYNAMIC_FEATURES_ALLOW; + } + + $can_use_dynamic_features = apply_filters( 'pods_access_can_use_dynamic_features', null, $pod ); + + if ( is_bool( $can_use_dynamic_features ) ) { + return $can_use_dynamic_features; + } + + $dynamic_features_allow = true; + + if ( $pod instanceof Pod ) { + $dynamic_features_allow = pods_is_type_public( + [ + 'pod' => $pod, + ] + ); + } + + 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( $type ) { + if ( ! pods_can_use_dynamic_features() ) { + return false; + } + + if ( empty( $type ) ) { + return false; + } + + // Handle the constants. + if ( 'view' === $type && defined( 'PODS_SHORTCODE_ALLOW_VIEWS' ) && ! PODS_SHORTCODE_ALLOW_VIEWS ) { + return false; + } + + $can_use_dynamic_feature = apply_filters( 'pods_access_can_use_dynamic_feature', null, $type ); + + if ( is_bool( $can_use_dynamic_feature ) ) { + return $can_use_dynamic_feature; + } + + $dynamic_features_enabled = [ + 'display', + 'form', + ]; + + $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, $type, $mode = null ) { + if ( ! pods_can_use_dynamic_feature( $type ) ) { + return false; + } + + if ( defined( 'PODS_DYNAMIC_FEATURES_RESTRICT' ) && ! PODS_DYNAMIC_FEATURES_RESTRICT ) { + return true; + } + + $can_use_dynamic_features_unrestricted = apply_filters( 'pods_access_can_use_dynamic_features_unrestricted', null, $args, $type, $mode ); + + if ( is_bool( $can_use_dynamic_features_unrestricted ) ) { + return $can_use_dynamic_features_unrestricted; + } + + $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'; + } + + if ( ! empty( $type ) ) { + $restricted_dynamic_features = $default_restricted_dynamic_features; + + 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; + } + + $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 ) { + $restricted_dynamic_features_forms = $default_restricted_dynamic_features_forms; + + 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; + } + + $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, $force_message = false ) { + $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 ''; + } + + return ''; +} + +/** + * 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, $force_message = false, $message = null ) { + $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 '' . $message; + } + + // Check if constant is hiding all notices. + if ( ! $force_message && defined( 'PODS_ACCESS_HIDE_NOTICES' ) && PODS_ACCESS_HIDE_NOTICES ) { + return ''; + } + + return ''; +} + +/** + * 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 = [] ) { + // 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 = 'restricted'; + } + + 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 = '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 bleep placeholder text. + * + * @since 3.1.0 + * + * @return string The bleep placeholder text. + */ +function pods_access_bleep_placeholder() { + 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( $sql, $context, array $args = [] ) { + $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( $allowed, $sql ) { + 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( $allowed, $sql ) { + 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( $allowed, $sql ) { + 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( $allowed, $sql ) { + 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( $allowed, $sql ) { + 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( $allowed, $sql, $context, array $info ) { + 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 d1c40af162..bf9f815862 100644 --- a/includes/classes.php +++ b/includes/classes.php @@ -198,14 +198,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 4a3e05e45b..7fd2ec35ec 100644 --- a/includes/data.php +++ b/includes/data.php @@ -358,6 +358,7 @@ function pods_v( $var = null, $type = 'get', $default = null, $strict = false, $ $defaults = array( 'casting' => false, 'allowed' => null, + 'source' => null, ); $params = (object) array_merge( $defaults, (array) $params ); @@ -376,6 +377,46 @@ function pods_v( $var = null, $type = 'get', $default = null, $strict = false, $ } } else { $type = strtolower( (string) $type ); + + if ( $params->source ) { + // Using keys for faster isset() checks instead of in_array(). + $disallowed_types = []; + + if ( 'magic-tag' === $params->source ) { + $disallowed_types = [ + 'server' => false, + 'session' => false, + 'global' => false, + 'globals' => false, + 'cookie' => false, + 'constant' => false, + 'option' => false, + 'site-option' => false, + 'transient' => false, + 'site-transient' => false, + 'cache' => false, + 'pods-transient' => false, + 'pods-site-transient' => false, + 'pods-cache' => false, + 'pods-option-cache' => false, + ]; + } + + /** + * Allow filtering the list of disallowed variable types for the source. + * + * @since 2.9.4 + * + * @param array $disallowed_types The list of disallowed variable types for the source. + * @param string $source The source calling pods_v(). + */ + $disallowed_types = apply_filters( "pods_v_disallowed_types_for_source_{$params->source}", $disallowed_types, $params->source ); + + if ( isset( $disallowed_types[ $type ] ) ) { + return $default; + } + } + switch ( $type ) { case 'get': if ( isset( $_GET[ $var ] ) ) { @@ -616,6 +657,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': @@ -629,13 +672,15 @@ function pods_v( $var = null, $type = 'get', $default = null, $strict = false, $ } break; case 'user': + // Prevent deprecation notice from WP. + if ( 'id' === $var ) { + $var = 'ID'; + } + if ( is_user_logged_in() ) { $user = get_userdata( get_current_user_id() ); - // Prevent deprecation notice from WP. - if ( 'id' === $var ) { - $var = 'ID'; - } + $user = pods_access_bleep_data( $user ); if ( isset( $user->{$var} ) ) { $value = $user->{$var}; @@ -654,7 +699,10 @@ function pods_v( $var = null, $type = 'get', $default = null, $strict = false, $ } elseif ( ! is_array( $value ) && 0 < strlen( $value ) ) { $output = $value; } - }//end if + } elseif ( 'ID' === $var ) { + // Return 0 when logged out and calling the ID. + $output = 0; + } break; case 'option': $output = get_option( $var, $default ); @@ -1131,7 +1179,7 @@ function pods_query_arg( $array = null, $allowed = null, $excluded = null, $url $allowed = array_unique( array_merge( $pods_query_args['allowed'], $allowed ) ); $excluded = array_unique( array_merge( $pods_query_args['excluded'], $excluded ) ); - if ( ! isset( $_GET ) ) { + if ( ! isset( $_GET ) || $url ) { $query_args = array(); } else { $query_args = pods_unsanitize( $_GET ); @@ -1252,6 +1300,7 @@ function pods_cast( $value, $cast_from = null ) { * @since 1.8.9 */ function pods_create_slug( $orig, $strict = true ) { + $str = remove_accents( $orig ); $str = preg_replace( '/([_ \\/])/', '-', trim( $orig ) ); if ( $strict ) { @@ -1351,7 +1400,12 @@ function pods_unique_slug( $slug, $column_name, $pod, $pod_id = 0, $id = 0, $obj * @since 1.2.0 */ function pods_clean_name( $orig, $lower = true, $trim_underscores = false ) { - $str = trim( $orig ); + if ( null === $orig ) { + return ''; + } + + $str = trim( (string) $orig ); + $str = remove_accents( $str ); $str = preg_replace( '/([^0-9a-zA-Z\-_\s])/', '', $str ); $str = preg_replace( '/(\s_)/', '_', $str ); $str = preg_replace( '/(\s+)/', '_', $str ); @@ -1416,6 +1470,12 @@ function pods_js_camelcase_name( $orig ) { * @since 2.0.0 */ function pods_absint( $maybeint, $strict = true, $allow_negative = false ) { + if ( is_null( $maybeint ) ) { + $maybeint = 0; + } elseif ( is_bool( $maybeint ) ) { + $maybeint = (int) $maybeint; + } + if ( true === $strict && ! is_numeric( trim( $maybeint ) ) ) { return 0; } @@ -1777,24 +1837,43 @@ function pods_evaluate_tag( $tag, $args = array() ) { 'prefix', ); + $pods_v_var = ''; + $pods_v_type = 'get'; + if ( in_array( $tag[0], $single_supported, true ) ) { - $value = pods_v( '', $tag[0], null ); + $pods_v_type = $tag[0]; } elseif ( 1 === count( $tag ) ) { - $value = pods_v( $tag[0], 'get', null ); + $pods_v_var = $tag[0]; } elseif ( 2 === count( $tag ) ) { - $value = pods_v( $tag[1], $tag[0], null ); + $pods_v_var = $tag[1]; + $pods_v_type = $tag[0]; } else { // Some magic tags support traversal. - $value = pods_v( array_slice( $tag, 1 ), $tag[0], null ); + $pods_v_var = array_slice( $tag, 1 ); + $pods_v_type = $tag[0]; } + $value = pods_v( $pods_v_var, $pods_v_type, null, false, [ + 'source' => 'magic-tag', + ] ); + if ( $helper ) { if ( ! $pod instanceof Pods ) { $pod = pods(); } + $value = $pod->helper( $helper, $value ); } + /** + * Allow filtering the evaluated tag value. + * + * @since unknown + * + * @param mixed $value The evaluated tag value. + * @param string $tag The evaluated tag name. + * @param null|mixed $fallback The fallback value to use if not set, should already be sanitized. + */ $value = apply_filters( 'pods_evaluate_tag', $value, $tag, $fallback ); if ( is_array( $value ) && 1 === count( $value ) ) { @@ -1905,15 +1984,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 04e5b2906d..3bf47f53c6 100644 --- a/includes/general.php +++ b/includes/general.php @@ -784,7 +784,7 @@ function pods_help( $text, $url = null ) { return; } - if ( 0 < strlen( $url ) ) { + if ( $url && 0 < strlen( $url ) ) { $text .= '

    ' . esc_html__( 'Find out more', 'pods' ) . ' »'; } @@ -1031,6 +1031,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, $content = null, $check_display_access_rights = true ) { pods_doing_shortcode( true ); $return_exception = static function() { @@ -1040,9 +1056,36 @@ 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 ); - } catch ( Exception $exception ) { + $return = pods_shortcode_run( $tags, $content, $blog_is_switched, $check_display_access_rights ); + } catch ( Exception $throwable ) { + /** + * Allow filtering whether to throw errors for the shortcode. + * + * @since 3.0.9 + * + * @param bool $throw_errors Whether to throw errors for the shortcode. + */ + $throw_errors = apply_filters( 'pods_shortcode_throw_errors', false ); + + if ( $throw_errors ) { + if ( $blog_is_switched ) { + restore_current_blog(); + } + + throw $throwable; + } + $return = ''; if ( pods_is_debug_display() ) { @@ -1050,13 +1093,13 @@ function pods_shortcode( $tags, $content = null ) { sprintf( '%1$s: %2$s', esc_html__( 'Pods Renderer Error', 'pods' ), - esc_html( $exception->getMessage() ) + esc_html( $throwable->getMessage() ) ), 'error', true ); - $return .= '
    ' . esc_html( $exception->getTraceAsString() ) . '
    '; + $return .= '
    ' . esc_html( $throwable->getTraceAsString() ) . '
    '; } elseif ( is_user_logged_in() && ( @@ -1071,7 +1114,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 @@ -1079,6 +1122,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 ); @@ -1141,13 +1188,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 ''; } @@ -1216,13 +1271,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 ); @@ -1233,11 +1303,15 @@ function pods_shortcode_run( $tags, $content = null ) { } // Allow views only if not targeting a file path (must be within theme) - if ( 0 < strlen( $tags['view'] ) ) { + 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 ); @@ -1259,14 +1333,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'] ) ) { @@ -1299,11 +1384,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 + ); } } @@ -1320,11 +1409,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 ) ) { @@ -1368,11 +1461,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; @@ -1380,47 +1477,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( + $shortcode_allow_evaluate_tags = pods_shortcode_allow_evaluate_tags(); + + $evaluate_tags_args = [ 'sanitize' => true, 'fallback' => '""', 'use_current_pod' => true, - ); + ]; - if ( 0 < strlen( $tags['orderby'] ) ) { - $params['orderby'] = $tags['orderby']; + if ( $tags['select'] && 0 < strlen( (string) $tags['select'] ) ) { + if ( ! 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 + ); + } + + $params['select'] = $tags['select']; + } + + if ( $tags['join'] && 0 < strlen( (string) $tags['join'] ) ) { + if ( ! 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 + ); + } + + $params['join'] = $tags['join']; } - if ( 0 < strlen( $tags['where'] ) ) { + if ( $tags['where'] && 0 < strlen( (string) $tags['where'] ) ) { + $tags['where'] = ltrim( $tags['where'], ')' ); + + if ( ! 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 + ); + } + $params['where'] = $tags['where']; - if ( pods_shortcode_allow_evaluate_tags() ) { + if ( $shortcode_allow_evaluate_tags ) { $params['where'] = pods_evaluate_tags_sql( html_entity_decode( $params['where'] ), $evaluate_tags_args ); } } - if ( 0 < strlen( $tags['having'] ) ) { + if ( $tags['groupby'] && 0 < strlen( (string) $tags['groupby'] ) ) { + if ( ! 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 + ); + } + + $params['groupby'] = $tags['groupby']; + } + + if ( $tags['having'] && 0 < strlen( (string) $tags['having'] ) ) { + $tags['having'] = ltrim( $tags['having'], ')' ); + + if ( ! 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 + ); + } + $params['having'] = $tags['having']; - if ( pods_shortcode_allow_evaluate_tags() ) { + if ( $shortcode_allow_evaluate_tags ) { $params['having'] = pods_evaluate_tags_sql( html_entity_decode( $params['having'] ), $evaluate_tags_args ); } } - if ( 0 < strlen( $tags['groupby'] ) ) { - $params['groupby'] = $tags['groupby']; - } + if ( $tags['orderby'] && 0 < strlen( (string) $tags['orderby'] ) ) { + if ( ! 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 + ); + } - if ( 0 < strlen( $tags['select'] ) ) { - $params['select'] = $tags['select']; - } - if ( 0 < strlen( $tags['join'] ) ) { - $params['join'] = $tags['join']; + $params['orderby'] = $tags['orderby']; } - }//end if + } // Load filters and return HTML for later use. if ( @@ -1482,40 +1713,34 @@ 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', + // 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. * @@ -1532,17 +1757,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? @@ -1552,10 +1792,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. * @@ -1574,14 +1810,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 ) { @@ -1590,10 +1830,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. * @@ -1645,7 +1881,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'] ); @@ -1664,7 +1900,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 ); @@ -1672,10 +1908,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. * @@ -3520,10 +3752,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 0d862e1bb3..b4a9e99843 100644 --- a/init.php +++ b/init.php @@ -4,13 +4,13 @@ * * @package Pods * @author Pods Framework Team - * @copyright 2022 Pods Foundation, Inc + * @copyright 2023 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: 2.8.23 + * Version: 2.8.23.1 * Author: Pods Framework Team * Author URI: https://pods.io/about/ * Text Domain: pods @@ -43,9 +43,9 @@ add_action( 'init', 'pods_deactivate_pods_ui' ); } else { // Current version. - define( 'PODS_VERSION', '2.8.23' ); + define( 'PODS_VERSION', '2.8.23.1' ); - // Current database version, this is the last version the database changed. + // Current database version, this is the last version we had a database migration added in the /sql/ directory. define( 'PODS_DB_VERSION', '2.3.5' ); /** @@ -64,7 +64,7 @@ * * Found at: https://wordpress.org/about/stats/ * - * Next planned minimum PHP version: 7.0 + * Next planned minimum PHP version: 7.2 (to match WooCommerce and others pushing WP forward). */ if ( ! defined( 'PODS_PHP_VERSION_MINIMUM' ) ) { define( 'PODS_PHP_VERSION_MINIMUM', '5.6' ); @@ -96,6 +96,7 @@ spl_autoload_register( array( 'PodsInit', 'autoload_class' ) ); // 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 b42ee6229d..c5c4a9ae57 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pods", - "version": "2.8.23", + "version": "2.8.23.1", "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 ef4616fed1..658b6b9f9e 100644 --- a/readme.txt +++ b/readme.txt @@ -5,7 +5,7 @@ Tags: pods, custom post types, custom taxonomies, content types, custom fields, Requires at least: 5.5 Tested up to: 6.0 Requires PHP: 5.6 -Stable tag: 2.8.23 +Stable tag: 2.8.23.1 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -156,6 +156,25 @@ Pods really wouldn't be where it is without all the contributions from our [dono == Changelog == += 2.8.23.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. + +Upgrade now to Pods 3.1 to get the full benefits of the new Access Rights feature with additional customization settings available. + +* 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) +* Security hardening: All SQL fragments used by Dynamic Features are checked for disallowed usage like subqueries. (@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 > 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) + = 2.8.23 - July 4th, 2022 = * Tweak: Added support for exporting post types and taxonomies to PHP for debugging purposes. While logged in as an admin, add `?pods_debug_register_export=1` to expose textareas on the page to get the code. (@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/Blocks/API.php b/src/Pods/Blocks/API.php index e659535121..2b06286950 100644 --- a/src/Pods/Blocks/API.php +++ b/src/Pods/Blocks/API.php @@ -79,11 +79,23 @@ 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.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' ); + } + + // 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 171fc3ddd4..f2883913cd 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 ); } @@ -190,4 +194,34 @@ public function should_preload_block( $attributes = [], $block = null ) { */ return (bool) apply_filters( 'pods_blocks_types_preload_block', true, $this ); } + + /** + * Determine whether the block is being rendered in editor mode. + * + * @param array $attributes The block attributes used. + * + * @return bool Whether the block is being rendered in editor mode. + */ + public function in_editor_mode( $attributes = [] ) { + if ( ! empty( $attributes['_is_editor'] ) && ! empty( $attributes['_is_editor_mode'] ) ) { + return true; + } + + if ( is_admin() ) { + $screen = get_current_screen(); + + if ( $screen && 'post' === $screen->base ) { + return true; + } + } + + if ( + wp_is_json_request() + && did_action( 'rest_api_init' ) + ) { + return true; + } + + return false; + } } diff --git a/src/Pods/Blocks/Types/Field.php b/src/Pods/Blocks/Types/Field.php index a8984dc06d..ce3b59641b 100644 --- a/src/Pods/Blocks/Types/Field.php +++ b/src/Pods/Blocks/Types/Field.php @@ -139,7 +139,7 @@ public function render( $attributes = [], $content = '', $block = null ) { $attributes = array_map( 'pods_trim', $attributes ); if ( empty( $attributes['field'] ) ) { - if ( wp_is_json_request() && did_action( 'rest_api_init' ) ) { + if ( $this->in_editor_mode( $attributes ) ) { return $this->render_placeholder( '' . esc_html__( 'Pods Field Value', 'pods' ), esc_html__( 'Please specify a "Field Name" under "More Settings" to configure this block.', 'pods' ) @@ -161,6 +161,9 @@ public function render( $attributes = [], $content = '', $block = null ) { $attributes['use_current'] = false; } + $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. $attributes['name'] = $block->context['postType']; @@ -171,12 +174,11 @@ public function render( $attributes = [], $content = '', $block = null ) { unset( $attributes['use_current'] ); } } elseif ( - ! empty( $attributes['use_current'] ) - && ! empty( $_GET['post_id'] ) - && wp_is_json_request() - && did_action( 'rest_api_init' ) + $attributes['use_current'] + && 0 !== $provided_post_id + && $this->in_editor_mode( $attributes ) ) { - $attributes['slug'] = absint( $_GET['post_id'] ); + $attributes['slug'] = $provided_post_id; if ( empty( $attributes['name'] ) ) { $attributes['name'] = get_post_type( $attributes['slug'] ); diff --git a/src/Pods/Blocks/Types/Item_List.php b/src/Pods/Blocks/Types/Item_List.php index 54933b964d..e7d63f78e5 100644 --- a/src/Pods/Blocks/Types/Item_List.php +++ b/src/Pods/Blocks/Types/Item_List.php @@ -372,7 +372,7 @@ public function render( $attributes = [], $content = '', $block = null ) { $attributes = array_map( 'pods_trim', $attributes ); if ( empty( $attributes['template'] ) && empty( $attributes['template_custom'] ) ) { - if ( wp_is_json_request() && did_action( 'rest_api_init' ) ) { + if ( $this->in_editor_mode( $attributes ) ) { return $this->render_placeholder( '' . esc_html__( 'Pods Item List', 'pods' ), esc_html__( 'Please specify a "Template" or "Custom Template" under "More Settings" to configure this block.', 'pods' ) @@ -392,15 +392,17 @@ public function render( $attributes = [], $content = '', $block = null ) { $attributes['name'] = $block->context['postType']; } + $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 ( - ! empty( $_GET['post_id'] ) - && wp_is_json_request() - && did_action( 'rest_api_init' ) + 0 !== $provided_post_id + && $this->in_editor_mode( $attributes ) ) { - $post_id = absint( $_GET['post_id'] ); + $attributes['slug'] = $provided_post_id; - $attributes['name'] = get_post_type( $post_id ); + $attributes['name'] = get_post_type( $attributes['slug'] ); } else { $attributes['name'] = get_post_type(); } diff --git a/src/Pods/Blocks/Types/Item_Single.php b/src/Pods/Blocks/Types/Item_Single.php index aa9a468829..ef7ca46eab 100644 --- a/src/Pods/Blocks/Types/Item_Single.php +++ b/src/Pods/Blocks/Types/Item_Single.php @@ -224,7 +224,7 @@ public function render( $attributes = [], $content = '', $block = null ) { empty( $attributes['template'] ) && empty( $attributes['template_custom'] ) ) { - if ( wp_is_json_request() && did_action( 'rest_api_init' ) ) { + if ( $this->in_editor_mode( $attributes ) ) { return $this->render_placeholder( '' . esc_html__( 'Pods Single Item', 'pods' ), esc_html__( 'Please specify a "Template" or "Custom Template" under "More Settings" to configure this block.', 'pods' ) @@ -246,6 +246,9 @@ public function render( $attributes = [], $content = '', $block = null ) { $attributes['use_current'] = false; } + $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. $attributes['name'] = $block->context['postType']; @@ -257,11 +260,10 @@ public function render( $attributes = [], $content = '', $block = null ) { } } elseif ( ! empty( $attributes['use_current'] ) - && ! empty( $_GET['post_id'] ) - && wp_is_json_request() - && did_action( 'rest_api_init' ) + && 0 !== $provided_post_id + && $this->in_editor_mode( $attributes ) ) { - $attributes['slug'] = absint( $_GET['post_id'] ); + $attributes['slug'] = $provided_post_id; if ( empty( $attributes['name'] ) ) { $attributes['name'] = get_post_type( $attributes['slug'] ); diff --git a/src/Pods/Data/Map_Field_Values.php b/src/Pods/Data/Map_Field_Values.php index 5f6a1fd274..2a56c9090a 100644 --- a/src/Pods/Data/Map_Field_Values.php +++ b/src/Pods/Data/Map_Field_Values.php @@ -244,10 +244,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 null; - } - $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/Whatsit/Storage/Post_Type.php b/src/Pods/Whatsit/Storage/Post_Type.php index 0d622384b6..53c11ac108 100644 --- a/src/Pods/Whatsit/Storage/Post_Type.php +++ b/src/Pods/Whatsit/Storage/Post_Type.php @@ -555,7 +555,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/PodsTest.php b/tests/codeception/wpunit/Pods/PodsTest.php index 32efce2e88..14dd6d666e 100644 --- a/tests/codeception/wpunit/Pods/PodsTest.php +++ b/tests/codeception/wpunit/Pods/PodsTest.php @@ -12,17 +12,89 @@ 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 ); + remove_all_filters( 'pods_session_id' ); + + parent::tearDown(); } /** @@ -66,15 +138,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 +424,89 @@ 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 ); + + $this->non_public_pod = pods( $this->non_public_pod_name ); + + // test shortcode + $output = $this->non_public_pod->form(); + + $this->assertContains( '