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