JezK
Edit File: flows.php
<?php /** * CartFlows Flows Query. * * @package CartFlows */ namespace CartflowsAdmin\AdminCore\Api; // Exit if accessed directly. if ( ! defined( 'ABSPATH' ) ) { exit; } use CartflowsAdmin\AdminCore\Api\ApiBase; /** * Class Admin_Query. */ class Flows extends ApiBase { /** * Route base. * * @var string */ protected $rest_base = '/admin/flows/'; /** * Instance * * @access private * @var object Class object. * @since 1.0.0 */ private static $instance; /** * Initiator * * @since 1.0.0 * @return object initialized object of class. */ public static function get_instance() { if ( ! isset( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } /** * Init Hooks. * * @since 1.0.0 * @return void */ public function register_routes() { // Eg. http://example.local/wp-json/cartflows/v1/admin/flows/. $namespace = $this->get_api_namespace(); register_rest_route( $namespace, $this->rest_base, array( array( 'methods' => 'POST', // WP_REST_Server::READABLE. 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => array(), // get_collection_params may use. ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Get items * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response */ public function get_items( $request ) { $post_status = 'any'; $post_count = 10; if ( null !== $request->get_param( 'per_page' ) ) { $post_count = min( absint( $request->get_param( 'per_page' ) ), 100 ); } $args = array( 'post_type' => CARTFLOWS_FLOW_POST_TYPE, 'post_status' => $post_status, 'orderby' => 'ID', ); // checking if store checkout is available and removing it from the list of flows. $store_checkout_id = intval( \Cartflows_Helper::get_global_setting( '_cartflows_store_checkout' ) ); if ( 0 !== $store_checkout_id ) { $args['post__not_in'] = array( $store_checkout_id ); } if ( null !== $request->get_param( 'paged' ) ) { $args['paged'] = absint( $request->get_param( 'paged' ) ); } $mode = $request->get_param( 'mode' ); if ( null !== $mode ) { if ( 'sandbox' === $mode ) { $args['meta_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query array( 'key' => 'wcf-testing', 'value' => 'yes', ), ); } else { $args['meta_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'relation' => 'OR', array( 'key' => 'wcf-testing', 'value' => 'no', ), array( 'key' => 'wcf-testing', 'compare' => 'NOT EXISTS', ), ); } } if ( 'any' === $post_status ) { if ( null !== $request->get_param( 's' ) ) { $args['s'] = sanitize_text_field( $request->get_param( 's' ) ); } if ( null !== $request->get_param( 'post_status' ) ) { $status = $request->get_param( 'post_status' ); // Allowlist of valid post statuses for flows. $allowed_statuses = array( 'active', 'inactive', 'publish', 'draft', 'pending', 'trash' ); if ( 'active' === $status ) { $args['post_status'] = 'publish'; } elseif ( 'inactive' === $status ) { $args['post_status'] = 'draft'; } elseif ( in_array( $status, $allowed_statuses, true ) ) { $args['post_status'] = sanitize_text_field( $status ); } // Invalid statuses are silently ignored, falling through to the default below. } if ( ! isset( $args['post_status'] ) || null === $request->get_param( 'post_status' ) ) { $args['post_status'] = 'publish'; } $start_date = $request->get_param( 'start_date' ); $end_date = $request->get_param( 'end_date' ); if ( ! empty( $start_date ) && ! empty( $end_date ) ) { $args['date_query'] = array( array( 'after' => sanitize_text_field( $start_date ) . ' 00:00:00', 'before' => sanitize_text_field( $end_date ) . ' 23:59:59', 'inclusive' => true, 'column' => 'post_date', ), ); } } if ( ! empty( $post_count ) ) { $args['posts_per_page'] = $post_count; } $result = new \WP_Query( $args ); $data = array( 'items' => array(), 'pagination' => array(), ); // On free, compute revenue for the whole page in a single grouped query instead of one query per flow. $is_free_revenue = ! _is_cartflows_pro() && ! is_wcf_pro_plan(); $revenue_map = array(); $zero_revenue = str_replace( ' ', '', wc_price( 0 ) ); if ( $is_free_revenue && ! empty( $result->posts ) ) { $revenue_map = $this->get_flows_revenue( wp_list_pluck( $result->posts, 'ID' ) ); } if ( $result->have_posts() ) { while ( $result->have_posts() ) { $result->the_post(); global $post; $post_data = (array) $post; // Modify the date Format just to display it. $post_data['post_modified'] = date_format( date_create( $post_data['post_modified'] ), 'yy/m/d' ); $post_data['post_status'] = ucwords( $post_data['post_status'] ); $first_step_url = \Cartflows_Flow_Post_Type::get_instance()->get_first_step_url( $post ); $view = $first_step_url ? $first_step_url : '#'; $edit = admin_url( 'admin.php?page=cartflows&path=flows&action=wcf-edit-flow&flow_id=' . $post->ID ); $delete = '#'; $clone = '#'; $export = '#'; $post_data['flow_test_mode'] = ( 'yes' === wcf()->options->get_flow_meta_value( $post->ID, 'wcf-testing' ) ) ? true : false; $flow_steps = get_post_meta( $post->ID, 'wcf-steps', true ); $post_data['step_count'] = is_array( $flow_steps ) ? count( $flow_steps ) : 0; $post_data['actions'] = array( 'view' => array( 'action' => 'edit', 'class' => '', 'attr' => array( 'target' => '_blank' ), 'text' => __( 'View', 'cartflows' ), 'link' => $view, ), 'edit' => array( 'action' => 'edit', 'class' => '', 'attr' => array(), 'text' => __( 'Edit', 'cartflows' ), 'link' => $edit, ), 'duplicate' => array( 'action' => 'clone', 'attr' => array(), 'class' => '', 'text' => __( 'Duplicate', 'cartflows' ), 'link' => $clone, ), 'export' => array( 'action' => 'export', 'attr' => array(), 'class' => '', 'text' => __( 'Export', 'cartflows' ), 'link' => $export, ), 'delete' => array( 'action' => 'delete', 'attr' => array(), 'class' => '', 'text' => __( 'Delete', 'cartflows' ), 'link' => $delete, ), ); // Fetch the revenue only for free version for the PRO it will be fetched and added by the filter later in the code. if ( $is_free_revenue ) { $post_data['revenue'] = isset( $revenue_map[ $post->ID ] ) ? $revenue_map[ $post->ID ] : $zero_revenue; } $data['items'][] = $post_data; } } $data['found_posts'] = $result->found_posts; $data['post_status'] = isset( $post_data['post_status'] ) ? $post_data['post_status'] : $args['post_status']; $data['active_flows_count'] = intval( wp_count_posts( CARTFLOWS_FLOW_POST_TYPE )->publish ); $data['trash_flows_count'] = intval( wp_count_posts( CARTFLOWS_FLOW_POST_TYPE )->trash ); $data['draft_flows_count'] = intval( wp_count_posts( CARTFLOWS_FLOW_POST_TYPE )->draft ); $data['pagination'] = array( 'found_posts' => $result->found_posts, 'paged' => $result->query['paged'], 'max_pages' => $result->max_num_pages, ); // Reducing count of active_flows_count if store checkout is set. if ( 0 !== $store_checkout_id ) { $data['active_flows_count']--; } wp_reset_postdata(); $data['status'] = true; // Retrieve the revenue data from the PRO to display it on the flow listing page. if ( _is_cartflows_pro() && is_wcf_pro_plan() ) { $data = apply_filters( 'cartflows_admin_flows_page_data', $data ); } if ( ! $result->have_posts() ) { $data['status'] = false; $response = new \WP_REST_Response( $data ); $response->set_status( 200 ); return $response; } $response = new \WP_REST_Response( $data ); $response->set_status( 200 ); return $response; } /** * Check whether a given request has permission to read notes. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) { return new \WP_Error( 'cartflows_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'cartflows' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Get revenue of flow. * * @param int $flow_id flow id. * @return int */ public function get_per_flow_revenue( $flow_id ) { $gross_sale = 0; // Return if WooCommerce is not active. if ( ! wcf()->is_woo_active ) { return $gross_sale; } // Fetch primary orders: Checkout, Order Bumps. $args = array( 'status' => array( 'completed', 'processing', 'cancelled' ), // Accepts a string: one of 'pending', 'processing', 'on-hold', 'completed', 'refunded, 'failed', 'cancelled', or a custom order status. 'meta_key' => '_wcf_flow_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => $flow_id, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 'meta_compare' => '=', // Possible values are ‘=’, ‘!=’, ‘>’, ‘>=’, ‘<‘, ‘<=’, ‘LIKE’, ‘NOT LIKE’, ‘IN’, ‘NOT IN’, ‘BETWEEN’, ‘NOT BETWEEN’, ‘EXISTS’ (only in WP >= 3.5), and ‘NOT EXISTS’ (also only in WP >= 3.5). Values ‘REGEXP’, ‘NOT REGEXP’ and ‘RLIKE’ were added in WordPress 3.7. Default value is ‘=’. 'return' => 'ids', // Accepts a string: 'ids' or 'objects'. Default: 'objects'. ); $orders = wc_get_orders( $args ); if ( ! empty( $orders ) && is_array( $orders ) ) { foreach ( $orders as $order_id ) { $order = wc_get_order( $order_id ); $user_id = $order->get_user_id(); // skip the orders which are placed by the user whose user role is Administrator. if ( $user_id && user_can( $user_id, 'cartflows_manage_flows_steps' ) ) { continue; } $order_total = $order->get_total(); if ( ! $order->has_status( 'cancelled' ) ) { $gross_sale += (float) $order_total; } } } return str_replace( ' ', '', wc_price( $gross_sale ) ); } /** * Get gross revenue for many flows in a single grouped query. * * Replaces the per-flow N+1 lookup; sums completed/processing order totals * grouped by `_wcf_flow_id`, excluding orders placed by flow managers. * * @since 3.1.3 * * @param int[] $flow_ids Flow IDs to fetch revenue for. * @return array<int,string> Map of flow ID => formatted revenue string. */ public function get_flows_revenue( $flow_ids ) { $revenue = array(); // Return if WooCommerce is not active. if ( ! function_exists( 'WC' ) ) { return $revenue; } $flow_ids = array_filter( array_map( 'absint', (array) $flow_ids ) ); if ( empty( $flow_ids ) ) { return $revenue; } global $wpdb; // Exclude orders placed by users who can manage flows (e.g. internal test orders). $excluded_user_ids = array_map( 'absint', get_users( array( 'capability' => 'cartflows_manage_flows_steps', 'fields' => 'ID', ) ) ); $decimals = wc_get_price_decimals(); $flow_placeholder = implode( ',', array_fill( 0, count( $flow_ids ), '%d' ) ); $query_args = $flow_ids; $is_hpos = class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' ) && \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled(); if ( $is_hpos ) { $order_table = $wpdb->prefix . 'wc_orders'; $order_meta_table = $wpdb->prefix . 'wc_orders_meta'; $exclude_clause = ''; if ( ! empty( $excluded_user_ids ) ) { $exclude_clause = ' AND o.customer_id NOT IN ( ' . implode( ',', array_fill( 0, count( $excluded_user_ids ), '%d' ) ) . ' )'; $query_args = array_merge( $query_args, $excluded_user_ids ); } //phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare $rows = $wpdb->get_results( $wpdb->prepare( "SELECT om.meta_value AS flow_id, ROUND( SUM( o.total_amount ), $decimals ) AS revenue FROM $order_table o INNER JOIN $order_meta_table om ON o.id = om.order_id AND om.meta_key = '_wcf_flow_id' WHERE o.type = 'shop_order' AND o.status IN ( 'wc-completed', 'wc-processing' ) AND om.meta_value IN ( $flow_placeholder ) $exclude_clause GROUP BY om.meta_value", $query_args ) ); //phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare } else { $order_table = $wpdb->prefix . 'posts'; $order_meta_table = $wpdb->prefix . 'postmeta'; $exclude_clause = ''; if ( ! empty( $excluded_user_ids ) ) { $exclude_clause = ' AND ( cu.meta_value IS NULL OR cu.meta_value NOT IN ( ' . implode( ',', array_fill( 0, count( $excluded_user_ids ), '%d' ) ) . ' ) )'; $query_args = array_merge( $query_args, $excluded_user_ids ); } //phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare $rows = $wpdb->get_results( $wpdb->prepare( "SELECT om.meta_value AS flow_id, ROUND( SUM( m.meta_value ), $decimals ) AS revenue FROM $order_table o INNER JOIN $order_meta_table m ON o.ID = m.post_id AND m.meta_key = '_order_total' INNER JOIN $order_meta_table om ON o.ID = om.post_id AND om.meta_key = '_wcf_flow_id' LEFT JOIN $order_meta_table cu ON o.ID = cu.post_id AND cu.meta_key = '_customer_user' WHERE o.post_type = 'shop_order' AND o.post_status IN ( 'wc-completed', 'wc-processing' ) AND om.meta_value IN ( $flow_placeholder ) $exclude_clause GROUP BY om.meta_value", $query_args ) ); //phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare } if ( ! empty( $rows ) && is_array( $rows ) ) { foreach ( $rows as $row ) { $revenue[ absint( $row->flow_id ) ] = str_replace( ' ', '', wc_price( (float) $row->revenue ) ); } } return $revenue; } }