[a-z_]+)', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( __CLASS__, 'list_rows' ), 'permission_callback' => array( __CLASS__, 'can_manage' ), 'args' => array( 'page' => array( 'type' => 'integer', 'default' => 1, 'minimum' => 1, ), 'per_page' => array( 'type' => 'integer', 'default' => 50, 'minimum' => 1, 'maximum' => 200, ), 'orderby' => array( 'type' => 'string', 'default' => 'id', ), 'order' => array( 'type' => 'string', 'default' => 'desc', 'enum' => array( 'asc', 'desc' ), ), 'filters' => array( 'type' => 'object', 'default' => array(), ), ), ) ); register_rest_route( self::NS, '/grid/(?P[a-z_]+)/save', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( __CLASS__, 'save_rows' ), 'permission_callback' => array( __CLASS__, 'can_manage' ), 'args' => array( 'changes' => array( 'type' => 'array', 'required' => true, ), ), ) ); register_rest_route( self::NS, '/bulk', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( __CLASS__, 'bulk' ), 'permission_callback' => array( __CLASS__, 'can_manage' ), ) ); register_rest_route( self::NS, '/undo', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( __CLASS__, 'undo_info' ), 'permission_callback' => array( __CLASS__, 'can_manage' ), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( __CLASS__, 'undo' ), 'permission_callback' => array( __CLASS__, 'can_manage' ), ), ) ); } /** * Resolve the entity from the route. * * @param WP_REST_Request $request Request. * @return BDBE_Entity|WP_Error */ protected static function entity( WP_REST_Request $request ) { $entity = BDBE_Plugin::entity( sanitize_key( (string) $request['entity'] ) ); if ( ! $entity ) { return new WP_Error( 'bdbe_entity', __( 'This grid is not available.', 'gridsmith' ), array( 'status' => 404 ) ); } return $entity; } /** * Sanitise filters for an entity. * * @param BDBE_Entity $entity Entity. * @param mixed $raw Raw filters. * @return array */ public static function filters( BDBE_Entity $entity, $raw ) { $raw = is_array( $raw ) ? $raw : array(); $out = array(); foreach ( $entity->filter_keys() as $key => $type ) { if ( ! isset( $raw[ $key ] ) || ! is_scalar( $raw[ $key ] ) ) { continue; } $value = (string) $raw[ $key ]; // REST params are already unslashed. if ( 'bool' === $type ) { $out[ $key ] = in_array( $value, array( '1', 'true', 'yes' ), true ); } elseif ( 'key' === $type ) { $out[ $key ] = sanitize_title( $value ); } else { $out[ $key ] = sanitize_text_field( $value ); } } return $out; } /** * GET rows. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ public static function list_rows( WP_REST_Request $request ) { $entity = self::entity( $request ); if ( is_wp_error( $entity ) ) { return $entity; } $args = array( 'page' => max( 1, (int) $request['page'] ), 'per_page' => min( 200, max( 1, (int) $request['per_page'] ) ), 'orderby' => sanitize_key( (string) $request['orderby'] ), 'order' => 'asc' === $request['order'] ? 'asc' : 'desc', 'filters' => self::filters( $entity, $request['filters'] ), ); $result = $entity->query( $args ); $total = (int) $result['total']; return rest_ensure_response( array( 'rows' => $entity->rows( $result['objects'], $args ), 'total' => $total, 'pages' => (int) max( 1, ceil( $total / $args['per_page'] ) ), 'page' => $args['page'], 'undo' => BDBE_Changes::undo_info(), ) ); } /** * POST changed cells. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ public static function save_rows( WP_REST_Request $request ) { $entity = self::entity( $request ); if ( is_wp_error( $entity ) ) { return $entity; } $changes = array(); $count = 0; foreach ( (array) $request['changes'] as $change ) { if ( ! is_array( $change ) || ! isset( $change['id'], $change['field'] ) ) { continue; } $id = absint( $change['id'] ); $field = sanitize_key( (string) $change['field'] ); if ( ! $id || '' === $field || ++$count > 2000 ) { continue; } // Values are sanitised per field type in BDBE_Fields::sanitize(). $changes[ $id ][ $field ] = isset( $change['value'] ) && is_scalar( $change['value'] ) ? (string) $change['value'] : ''; } if ( ! $changes ) { return new WP_Error( 'bdbe_empty', __( 'There are no changes to save.', 'gridsmith' ), array( 'status' => 400 ) ); } $result = BDBE_Changes::apply( $entity, $changes, array( /* translators: %s: entity label, e.g. Products. */ 'label' => sprintf( __( '%s: grid edit', 'gridsmith' ), $entity->label() ), ) ); $rows = array(); foreach ( array_merge( $result['saved'], array_keys( $result['errors'] ) ) as $id ) { $object = $entity->load( $id ); if ( $object ) { $rows[] = $entity->row( $object ); } } return rest_ensure_response( array( 'saved' => count( $result['saved'] ), 'changes' => count( $result['changes'] ), 'errors' => (object) $result['errors'], 'rows' => $rows, 'undo' => BDBE_Changes::undo_info(), ) ); } /** * POST one chunk of a bulk action. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ public static function bulk( WP_REST_Request $request ) { $entity = BDBE_Plugin::entity( 'product' ); $action = sanitize_key( (string) $request->get_param( 'action' ) ); $params = $request->get_param( 'params' ); $scope = self::scope( $entity, $request->get_param( 'scope' ) ); $token = preg_replace( '/[^a-f0-9\-]/', '', strtolower( (string) $request->get_param( 'token' ) ) ); $result = BDBE_Bulk::run( $action, is_array( $params ) ? $params : array(), $scope, absint( $request->get_param( 'offset' ) ), (string) $token ); if ( is_wp_error( $result ) ) { return $result; } $result['undo'] = BDBE_Changes::undo_info(); return rest_ensure_response( $result ); } /** * Sanitise a bulk scope. * * @param BDBE_Entity $entity Entity. * @param mixed $raw Raw scope. * @return array */ public static function scope( BDBE_Entity $entity, $raw ) { $raw = is_array( $raw ) ? $raw : array(); $scope = array( 'include_variations' => ! empty( $raw['include_variations'] ), ); if ( ! empty( $raw['ids'] ) && is_array( $raw['ids'] ) ) { $scope['ids'] = array_slice( array_values( array_filter( array_map( 'absint', $raw['ids'] ) ) ), 0, 5000 ); } else { $scope['filters'] = self::filters( $entity, isset( $raw['filters'] ) ? $raw['filters'] : array() ); } return $scope; } /** * GET undo info. * * @return WP_REST_Response */ public static function undo_info() { return rest_ensure_response( array( 'undo' => BDBE_Changes::undo_info() ) ); } /** * POST undo. * * @return WP_REST_Response|WP_Error */ public static function undo() { $result = BDBE_Changes::undo(); if ( is_wp_error( $result ) ) { return $result; } $result['undo'] = BDBE_Changes::undo_info(); return rest_ensure_response( $result ); } }