admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'preorder_pilot_search_products' ), 'loading' => __( 'Searching products…', 'preorder-pilot' ), 'empty' => __( 'No eligible products found.', 'preorder-pilot' ), 'error' => __( 'Search failed. Try again.', 'preorder-pilot' ), 'choose' => __( 'Choose a product', 'preorder-pilot' ), ) ); } /** * Url. * * @param string $tab Tab. * @param array $args Arguments. */ public static function url( $tab = 'overview', $args = array() ) { return add_query_arg( array_merge( array( 'page' => self::PAGE_SLUG, 'tab' => $tab, ), $args ), admin_url( 'admin.php' ) ); } /** * Request scalar. * * @param mixed $source Source. * @param mixed $key Key. * @param string $fallback Fallback value. */ private static function request_scalar( $source, $key, $fallback = '' ) { if ( ! is_array( $source ) || ! isset( $source[ $key ] ) ) { return $fallback; } $value = wp_unslash( $source[ $key ] ); return is_scalar( $value ) ? $value : $fallback; } /** * Request text. * * @param mixed $source Source. * @param mixed $key Key. * @param string $fallback Fallback value. */ private static function request_text( $source, $key, $fallback = '' ) { return sanitize_text_field( self::request_scalar( $source, $key, $fallback ) ); } /** * Request textarea. * * @param mixed $source Source. * @param mixed $key Key. * @param string $fallback Default. */ private static function request_textarea( $source, $key, $fallback = '' ) { return sanitize_textarea_field( self::request_scalar( $source, $key, $fallback ) ); } /** * Request key. * * @param mixed $source Source. * @param mixed $key Key. * @param string $fallback Default. */ private static function request_key( $source, $key, $fallback = '' ) { return sanitize_key( self::request_scalar( $source, $key, $fallback ) ); } /** * Request absint. * * @param mixed $source Source. * @param mixed $key Key. */ private static function request_absint( $source, $key ) { return absint( self::request_scalar( $source, $key, 0 ) ); } /** * Request id list. * * @param mixed $source Source. * @param mixed $key Key. */ private static function request_id_list( $source, $key ) { if ( ! is_array( $source ) || ! isset( $source[ $key ] ) ) { return array(); } $values = wp_unslash( $source[ $key ] ); if ( ! is_array( $values ) ) { $values = array( $values ); } $ids = array(); foreach ( $values as $value ) { if ( ! is_scalar( $value ) || ! preg_match( '/^[0-9]+$/D', (string) $value ) ) { continue; } $id = absint( $value ); if ( $id ) { $ids[] = $id; } } return array_values( array_unique( $ids ) ); } /** * Query product ids. * * @param array $args Arguments. */ private static function query_product_ids( $args = array() ) { global $wpdb; $args = wp_parse_args( $args, array( 'search' => '', 'configured' => false, 'statuses' => array( 'publish' ), 'limit' => 15, 'page' => 1, ) ); $statuses = array_values( array_intersect( (array) $args['statuses'], array( 'publish', 'private', 'draft', 'pending', 'future' ) ) ); if ( ! $statuses ) { $statuses = array( 'publish' ); } $limit = max( 1, min( 50, absint( $args['limit'] ) ) ); $page = max( 1, absint( $args['page'] ) ); $where = array( "p.post_type IN ('product','product_variation')" ); $params = array(); $where[] = 'p.post_status IN (' . implode( ',', array_fill( 0, count( $statuses ), '%s' ) ) . ')'; $params = array_merge( $params, $statuses ); if ( ! empty( $args['configured'] ) ) { $where[] = "EXISTS (SELECT 1 FROM {$wpdb->postmeta} pp_conf WHERE pp_conf.post_id = p.ID AND pp_conf.meta_key = '_preorder_pilot')"; } $search = trim( (string) $args['search'] ); if ( '' !== $search ) { $like = '%' . $wpdb->esc_like( $search ) . '%'; $search_where = array( 'p.post_title LIKE %s', 'parent.post_title LIKE %s', "EXISTS (SELECT 1 FROM {$wpdb->postmeta} sku WHERE sku.post_id = p.ID AND sku.meta_key = '_sku' AND sku.meta_value LIKE %s)" ); $params = array_merge( $params, array( $like, $like, $like ) ); if ( ctype_digit( $search ) ) { $search_where[] = 'p.ID = %d'; $params[] = absint( $search ); } $where[] = '(' . implode( ' OR ', $search_where ) . ')'; } $where_sql = implode( ' AND ', $where ); $count_sql = "SELECT COUNT(DISTINCT p.ID) FROM {$wpdb->posts} p LEFT JOIN {$wpdb->posts} parent ON parent.ID = p.post_parent WHERE {$where_sql}"; $total = (int) $wpdb->get_var( $wpdb->prepare( $count_sql, $params ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Built from fixed fragments; values use placeholders. $params_for_ids = array_merge( $params, array( $limit, ( $page - 1 ) * $limit ) ); $ids_sql = "SELECT DISTINCT p.ID FROM {$wpdb->posts} p LEFT JOIN {$wpdb->posts} parent ON parent.ID = p.post_parent WHERE {$where_sql} ORDER BY p.ID DESC LIMIT %d OFFSET %d"; return (object) array( 'ids' => array_map( 'absint', $wpdb->get_col( $wpdb->prepare( $ids_sql, $params_for_ids ) ) ), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Built from fixed fragments; values use placeholders. 'total' => $total, ); } /** * Authorize. * * @param mixed $action Action. */ private function authorize( $action ) { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'You cannot manage preorders.', 'preorder-pilot' ), '', array( 'response' => 403 ) ); } check_admin_referer( 'preorder_pilot_' . $action ); } /** * Redirect with a notice; shared with other admin handlers. * * @param string $tab Tab. * @param string $message Message. * @param bool $error Error. */ public static function notice_redirect( $tab, $message, $error = false ) { $key = wp_generate_uuid4(); set_transient( 'preorder_pilot_notice_' . get_current_user_id() . '_' . $key, array( 'message' => $message, 'error' => $error, 'edit' => null, ), MINUTE_IN_SECONDS ); wp_safe_redirect( self::url( $tab, array( 'pilot_notice' => $key ) ) ); exit; } /** * Redirect. * * @param mixed $tab Tab. * @param mixed $message Message. * @param bool $error Error. * @param mixed|null $edit Edit. */ private function redirect( $tab, $message, $error = false, $edit = null ) { $key = wp_generate_uuid4(); set_transient( 'preorder_pilot_notice_' . get_current_user_id() . '_' . $key, array( 'message' => $message, 'error' => $error, 'edit' => $edit, ), MINUTE_IN_SECONDS ); wp_safe_redirect( self::url( $tab, array( 'pilot_notice' => $key ) ) ); exit; } // phpcs:disable WordPress.Security.NonceVerification.Missing -- Each handler below calls authorize() (check_admin_referer) or check_ajax_referer() before reading input. /** * Save product. */ public function save_product() { $this->authorize( 'save_product' ); $id = self::request_absint( $_POST, 'product_id' ); if ( ! current_user_can( 'edit_post', $id ) ) { $this->redirect( 'products', __( 'This product cannot be edited by your account.', 'preorder-pilot' ), true ); } $data = PreOrder_Pilot_Products::get_settings( $id ); foreach ( PreOrder_Pilot_Products::field_definitions( wc_get_product( $id ) ) as $field => $definition ) { if ( 'checkbox' === $definition['type'] ) { $data[ $field ] = isset( $_POST[ $field ] ); } elseif ( isset( $_POST[ $field ] ) && ! is_array( $_POST[ $field ] ) ) { $data[ $field ] = 'textarea' === $definition['type'] ? self::request_textarea( $_POST, $field ) : self::request_text( $_POST, $field ); } } $result = PreOrder_Pilot_Products::save_settings( $id, $data ); $product = wc_get_product( $id ); $edit = is_wp_error( $result ) && $product ? array_merge( $data, array( 'product_id' => $id, 'product_name' => wp_strip_all_tags( $product->get_formatted_name() ), ) ) : null; $this->redirect( 'products', is_wp_error( $result ) ? $result->get_error_message() : __( 'Preorder saved. Your storefront now uses these settings.', 'preorder-pilot' ), is_wp_error( $result ), $edit ); } /** * Bulk products. */ public function bulk_products() { $this->authorize( 'bulk_products' ); $ids = self::request_id_list( $_POST, 'product_ids' ); $mode = self::request_key( $_POST, 'bulk_action' ); if ( ! $ids || ! in_array( $mode, array( 'always', 'disabled', 'availability_date', 'release' ), true ) ) { $this->redirect( 'products', __( 'Select products and choose a bulk action.', 'preorder-pilot' ), true ); } foreach ( $ids as $id ) { if ( ! wc_get_product( $id ) || ! current_user_can( 'edit_post', $id ) ) { $this->redirect( 'products', __( 'A product is missing or cannot be edited. No products were changed.', 'preorder-pilot' ), true ); } } $bulk_date = self::request_text( $_POST, 'bulk_date' ); if ( 'availability_date' === $mode && ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $bulk_date ) || is_wp_error( PreOrder_Pilot_Products::sanitize_settings( array_merge( PreOrder_Pilot_Products::defaults(), array( 'availability_date' => $bulk_date ) ) ) ) ) ) { $this->redirect( 'products', __( 'Choose a valid expected shipping date before applying the bulk update.', 'preorder-pilot' ), true ); } $changed = 0; $errors = array(); foreach ( $ids as $id ) { if ( 'release' === $mode ) { $result = PreOrder_Pilot_Emails::release_product( $id ); if ( is_wp_error( $result ) ) { $errors[] = '#' . $id . ': ' . $result->get_error_message(); } else { ++$changed; } continue; } $data = PreOrder_Pilot_Products::get_settings( $id ); if ( 'availability_date' === $mode ) { $data['availability_date'] = $bulk_date; } else { $data['mode'] = $mode; } $result = PreOrder_Pilot_Products::save_settings( $id, $data ); if ( is_wp_error( $result ) ) { $errors[] = '#' . $id . ': ' . $result->get_error_message(); } else { ++$changed; } } $this->redirect( 'products', sprintf( /* translators: %d: Number of products updated. */ _n( '%d product updated.', '%d products updated.', $changed, 'preorder-pilot' ), $changed ) . ' ' . implode( ' ', $errors ), (bool) $errors ); } /** * Save settings. */ public function save_settings() { $this->authorize( 'save_settings' ); $settings = get_option( self::OPTION_NAME, array() ); if ( ! is_array( $settings ) ) { $settings = array(); } foreach ( array( 'default_message', 'default_button' ) as $field ) { $settings[ $field ] = self::request_text( $_POST, $field ); } $settings['availability_message'] = $settings['default_message']; $settings['preorder_status'] = isset( $_POST['preorder_status'] ); $settings['keep_data'] = ! isset( $_POST['delete_data'] ); /** * Lets add-ons save the settings fields they render. The request nonce is already verified. * * @param array $settings Settings. */ $settings = (array) apply_filters( 'preorder_pilot_save_store_settings', $settings ); update_option( self::OPTION_NAME, $settings ); $this->redirect( 'settings', __( 'Store defaults saved.', 'preorder-pilot' ) ); } /** * Search products. */ public function search_products() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( array(), 403 ); } check_ajax_referer( 'preorder_pilot_search_products', 'nonce' ); $result = self::query_product_ids( array( 'search' => self::request_text( $_GET, 'search' ), 'configured' => false, 'statuses' => array( 'publish' ), 'limit' => 40, ) ); $rows = array(); foreach ( $result->ids as $id ) { $product = wc_get_product( $id ); if ( ! $product || ! $product->is_type( PreOrder_Pilot_Products::supported_types() ) || ! current_user_can( 'edit_post', $id ) ) { continue; } $rows[] = array( 'id' => $id, 'name' => wp_strip_all_tags( $product->get_formatted_name() ), ); } wp_send_json_success( $rows ); } /** * Toggle product. */ public function toggle_product() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( array( 'message' => __( 'You cannot manage preorders.', 'preorder-pilot' ) ), 403 ); } check_ajax_referer( 'preorder_pilot_search_products', 'nonce' ); $id = self::request_absint( $_POST, 'product_id' ); if ( ! current_user_can( 'edit_post', $id ) ) { wp_send_json_error( array( 'message' => __( 'You cannot edit this product.', 'preorder-pilot' ) ), 403 ); } $data = PreOrder_Pilot_Products::get_settings( $id ); $data['mode'] = 'disabled' === $data['mode'] ? 'always' : 'disabled'; $result = PreOrder_Pilot_Products::save_settings( $id, $data ); if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 ); } $message = 'disabled' === $data['mode'] ? __( 'Preorder paused.', 'preorder-pilot' ) : __( 'Preorder enabled.', 'preorder-pilot' ); $key = wp_generate_uuid4(); set_transient( 'preorder_pilot_notice_' . get_current_user_id() . '_' . $key, array( 'message' => $message, 'error' => false, ), MINUTE_IN_SECONDS ); wp_send_json_success( array( 'message' => $message, 'redirect' => self::url( 'products', array( 'pilot_notice' => $key ) ), ) ); } // phpcs:enable WordPress.Security.NonceVerification.Missing /** * Render page. */ public function render_page() { if ( ! current_user_can( 'manage_woocommerce' ) ) { return; } $tabs = $this->tabs(); $tab = self::request_key( $_GET, 'tab', 'overview' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only navigation/display parameter; no state change. if ( ! isset( $tabs[ $tab ] ) ) { $tab = 'overview'; } ?>
PreOrder Pilot . 'assets/mascot/mascot-transparent-original.png' ); ?>)
| #get_order_number() ); ?> | get_formatted_billing_full_name() ? $order->get_formatted_billing_full_name() : $order->get_billing_email() ); ?> | date_i18n( get_option( 'date_format' ) ) : '—' ); ?> | get_status() ) ); ?> | $order->get_currency() ) ) ); do_action( 'preorder_pilot_order_row_actions', $order ); ?> |