\d+)'; $this->route( '/campaigns', WP_REST_Server::READABLE, 'list_campaigns' ); $this->route( '/campaigns', WP_REST_Server::CREATABLE, 'create_campaign' ); $this->route( $id, WP_REST_Server::READABLE, 'get_campaign' ); $this->route( $id, WP_REST_Server::EDITABLE, 'update_campaign' ); $this->route( $id . '/status', WP_REST_Server::CREATABLE, 'set_status' ); $this->route( $id . '/codes', WP_REST_Server::READABLE, 'list_codes' ); $this->route( $id . '/issue', WP_REST_Server::CREATABLE, 'issue_codes' ); $this->route( '/categories', WP_REST_Server::READABLE, 'list_categories' ); $this->route( '/categories', WP_REST_Server::CREATABLE, 'create_category' ); $this->route( '/dashboard', WP_REST_Server::READABLE, 'dashboard' ); $this->route( '/lookup', WP_REST_Server::READABLE, 'lookup' ); $this->route( '/code-available', WP_REST_Server::READABLE, 'code_available' ); $this->route( '/promotions/(?P[a-z_]+)/dismiss', WP_REST_Server::CREATABLE, 'dismiss_promotion' ); } public function can_manage(): bool { return current_user_can( 'manage_woocommerce' ); } public function list_campaigns(): WP_REST_Response { $stats = $this->stats->all(); $categories = $this->categories->all(); $list = array(); foreach ( $this->campaigns->all() as $campaign ) { $list[] = $this->campaign_data( $campaign, $categories, $stats[ $campaign->id ] ?? CampaignStats::empty() ); } return new WP_REST_Response( $list ); } /** * @param WP_REST_Request $request Request. */ public function get_campaign( WP_REST_Request $request ) { $campaign = $this->campaigns->find( (int) $request['id'] ); if ( ! $campaign ) { return $this->not_found(); } $data = $this->campaign_data( $campaign, $this->categories->all(), $this->stats->for_campaign( $campaign->id ) ); $data['labels'] = $this->labels( $campaign ); $data['issue_batch'] = self::ISSUE_BATCH; return new WP_REST_Response( $data ); } /** * @param WP_REST_Request $request Request with the campaign as JSON. */ public function create_campaign( WP_REST_Request $request ) { $input = $this->input( $request ); $result = $this->campaigns->create( $input ); if ( $result instanceof WP_Error ) { return $this->error( $result ); } $this->save_extensions( $result->id, $input ); return $this->get_campaign( $this->with_id( $result->id ) ); } /** * @param WP_REST_Request $request Request with the campaign as JSON. */ public function update_campaign( WP_REST_Request $request ) { $input = $this->input( $request ); $result = $this->campaigns->update( (int) $request['id'], $input ); if ( $result instanceof WP_Error ) { return $this->error( $result ); } $this->save_extensions( $result->id, $input ); return $this->get_campaign( $this->with_id( $result->id ) ); } /** * @param WP_REST_Request $request Request with status 'active' or 'ended'. */ public function set_status( WP_REST_Request $request ) { if ( ! $this->campaigns->set_status( (int) $request['id'], sanitize_key( (string) $request['status'] ) ) ) { return $this->not_found(); } return $this->get_campaign( $this->with_id( (int) $request['id'] ) ); } /** * @param WP_REST_Request $request Request. */ public function list_codes( WP_REST_Request $request ) { if ( ! $this->campaigns->find( (int) $request['id'] ) ) { return $this->not_found(); } $codes = array(); foreach ( $this->codes->for_campaign( (int) $request['id'], 200 ) as $row ) { $codes[] = array( 'code' => (string) $row->code, 'email' => (string) $row->email, 'uses' => (int) $row->uses, 'expires_at' => $row->expires_at ? get_date_from_gmt( (string) $row->expires_at, 'Y-m-d' ) : '', 'created_at' => get_date_from_gmt( (string) $row->created_at, 'Y-m-d' ), 'source' => (string) $row->source, ); } return new WP_REST_Response( $codes ); } /** * Issue personal codes to a list of addresses (separated by new lines, commas, semicolons or spaces). * * @param WP_REST_Request $request Request with 'emails'. */ public function issue_codes( WP_REST_Request $request ) { $campaign = $this->campaigns->find( (int) $request['id'] ); if ( ! $campaign ) { return $this->not_found(); } $emails = self::split_emails( (string) $request['emails'] ); if ( count( $emails ) > self::ISSUE_BATCH ) { return new WP_Error( 'cnwscoupons_batch', __( 'Too many addresses in one request.', 'webstyle-coupons' ), array( 'status' => 400 ) ); } $results = array(); foreach ( $emails as $email ) { $code = $this->issuer->issue( $campaign->slug, $email, 'admin' ); $results[] = array( 'email' => $email, 'code' => is_string( $code ) ? $code : '', 'error' => is_string( $code ) ? '' : $code->get_error_message(), ); } return new WP_REST_Response( $results ); } public function list_categories(): WP_REST_Response { $counts = array(); foreach ( $this->campaigns->all() as $campaign ) { $counts[ $campaign->category_id ] = ( $counts[ $campaign->category_id ] ?? 0 ) + 1; } $list = array(); foreach ( $this->categories->all() as $id => $name ) { $list[] = array( 'id' => $id, 'name' => $name, 'campaigns' => $counts[ $id ] ?? 0, ); } return new WP_REST_Response( $list ); } /** * @param WP_REST_Request $request Request with 'name'. */ public function create_category( WP_REST_Request $request ) { $id = $this->categories->find_or_create( (string) $request['name'] ); return $id ? $this->list_categories() : new WP_Error( 'cnwscoupons_category_name', __( 'The category needs a name.', 'webstyle-coupons' ), array( 'status' => 400 ) ); } /** * Search products, product categories, tags or attribute values for the restriction pickers. * * @param WP_REST_Request $request Request with 'type' and 'search'. */ public function lookup( WP_REST_Request $request ): WP_REST_Response { $search = sanitize_text_field( (string) $request['search'] ); $page = max( 1, absint( $request['page'] ) ); switch ( sanitize_key( (string) $request['type'] ) ) { case 'products': return new WP_REST_Response( Lookup::products( $search, $page ) ); case 'categories': return new WP_REST_Response( Lookup::terms( 'product_cat', $search, $page ) ); case 'tags': return new WP_REST_Response( Lookup::terms( 'product_tag', $search, $page ) ); case 'attributes': return new WP_REST_Response( Lookup::attribute_terms( $search, $page ) ); } return new WP_REST_Response( array( 'items' => array(), 'more' => false, ) ); } /** * @param WP_REST_Request $request Request with 'code'. */ public function code_available( WP_REST_Request $request ): WP_REST_Response { $code = CampaignRepository::normalize_shared_code( (string) $request['code'] ); return new WP_REST_Response( array( 'code' => $code, 'valid' => '' !== $code, 'available' => '' !== $code && $this->campaigns->code_available( $code ), ) ); } /** * @param WP_REST_Request $request period (week, month, 3months, custom), from, to, category, campaign. */ public function dashboard( WP_REST_Request $request ) { $report = $this->dashboard->filtered( sanitize_key( (string) $request['period'] ), sanitize_text_field( (string) $request['from'] ), sanitize_text_field( (string) $request['to'] ), (int) $request['category'], absint( $request['campaign'] ) ); if ( null === $report ) { return new WP_Error( 'cnwscoupons_period', __( 'Choose a valid period of at most two years.', 'webstyle-coupons' ), array( 'status' => 400 ) ); } return new WP_REST_Response( $report ); } /** * @param WP_REST_Request $request Request with the promotion key. */ public function dismiss_promotion( WP_REST_Request $request ): WP_REST_Response { $this->promotions->dismiss( (string) $request['key'] ); return new WP_REST_Response( $this->promotions->visible() ); } /** * Campaign as the app sees it. * * @param Campaign $campaign Campaign. * @param array $categories Category names by ID. * @param array $stats CampaignStats row. */ private function campaign_data( Campaign $campaign, array $categories, array $stats ): array { $shared = $campaign->is_shared() ? $this->codes->find_for( $campaign->id, '' ) : null; return array( 'id' => $campaign->id, 'slug' => $campaign->slug, 'name' => $campaign->name, 'category' => $categories[ $campaign->category_id ] ?? '', 'category_id' => $campaign->category_id, 'type' => $campaign->type, 'discount_type' => $campaign->discount_type, 'amount' => $campaign->amount, 'starts_on' => $campaign->starts_on, 'expires_on' => $campaign->expires_on, 'valid_days' => $campaign->valid_days, 'code_prefix' => $campaign->code_prefix, 'max_codes' => $campaign->max_codes, 'uses_per_code' => $campaign->uses_per_code, 'max_redemptions' => $campaign->max_redemptions, 'max_per_email' => $campaign->max_per_email, 'email_restricted' => $campaign->email_restricted, 'restrictions' => $campaign->restrictions->to_array(), 'extensions' => (object) array_intersect_key( $campaign->extensions, self::extension_sanitizers() ), 'status' => $campaign->status, 'shared_code' => $shared ? (string) $shared->code : '', 'shared_uses' => $shared ? (int) $shared->uses : 0, 'stats' => $stats, ); } /** * Names for the IDs in a campaign's restrictions, so the pickers can show them. * * @param Campaign $campaign Campaign. * @return array> */ private function labels( Campaign $campaign ): array { $lists = $campaign->restrictions->lists; $ids = static fn( string $key ) => array_merge( $lists[ $key ]['include'], $lists[ $key ]['exclude'] ); return array( 'products' => Lookup::product_labels( $ids( 'products' ) ), 'categories' => Lookup::term_labels( $ids( 'categories' ) ), 'tags' => Lookup::term_labels( $ids( 'tags' ) ), 'attributes' => Lookup::term_labels( $ids( 'attributes' ), true ), ); } /** * Add-ons that store settings on a campaign: key => callback that cleans the submitted settings * and returns them as an array. Only registered keys are saved or shown to the app. * * @return array */ private static function extension_sanitizers(): array { $sanitizers = apply_filters( 'cnwscoupons_extension_sanitizers', array() ); return is_array( $sanitizers ) ? array_filter( $sanitizers, 'is_callable' ) : array(); } /** * @param int $campaign_id Campaign ID. * @param array $input Submitted campaign with an 'extensions' object. */ private function save_extensions( int $campaign_id, array $input ): void { $submitted = isset( $input['extensions'] ) && is_array( $input['extensions'] ) ? $input['extensions'] : array(); foreach ( self::extension_sanitizers() as $key => $sanitize ) { if ( isset( $submitted[ $key ] ) && is_array( $submitted[ $key ] ) ) { $clean = call_user_func( $sanitize, $submitted[ $key ] ); $this->campaigns->save_extension( $campaign_id, (string) $key, is_array( $clean ) ? $clean : array() ); } } } /** * @param WP_REST_Request $request Request with JSON body. */ private function input( WP_REST_Request $request ): array { $data = $request->get_json_params(); return is_array( $data ) ? $data : array(); } private function with_id( int $id ): WP_REST_Request { $request = new WP_REST_Request( 'GET' ); $request->set_url_params( array( 'id' => $id ) ); return $request; } /** * Split a pasted list of addresses, dropping duplicates. * * @param string $text Addresses separated by new lines, commas, semicolons or spaces. * @return string[] */ public static function split_emails( string $text ): array { $parts = preg_split( '/[\s,;]+/', strtolower( $text ) ); return array_values( array_unique( array_filter( array_map( 'trim', is_array( $parts ) ? $parts : array() ) ) ) ); } /** * @param string $path Route. * @param string $methods HTTP methods. * @param string $method Callback method on this class. */ private function route( string $path, string $methods, string $method ): void { register_rest_route( self::NAMESPACE, $path, array( 'methods' => $methods, 'callback' => array( $this, $method ), 'permission_callback' => array( $this, 'can_manage' ), ) ); } private function not_found(): WP_Error { return new WP_Error( 'cnwscoupons_not_found', __( 'The campaign does not exist.', 'webstyle-coupons' ), array( 'status' => 404 ) ); } /** * @param WP_Error $error Validation error. */ private function error( WP_Error $error ): WP_Error { $error->add_data( array( 'status' => 400 ) ); return $error; } }