self::FORM ); if ( $product_id ) { $args['product'] = (int) $product_id; } return add_query_arg( $args, admin_url( 'admin.php' ) ); } /** * URL of the product list. * * @return string */ public static function list_url() { return admin_url( 'edit.php?post_type=' . Paygrab_Product::TYPE ); } /** * The block editor is not used for products: send it to PAYGRAB's form. * * @return void */ public static function redirect_editor() { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- navigation only. $type = isset( $_GET['post_type'] ) ? sanitize_key( wp_unslash( $_GET['post_type'] ) ) : ''; $id = isset( $_GET['post'] ) ? absint( $_GET['post'] ) : 0; $act = isset( $_GET['action'] ) ? sanitize_key( wp_unslash( $_GET['action'] ) ) : ''; // phpcs:enable if ( 'load-post-new.php' === current_action() && Paygrab_Product::TYPE === $type ) { wp_safe_redirect( self::form_url() ); exit; } if ( $id && 'edit' === $act && Paygrab_Product::TYPE === get_post_type( $id ) ) { wp_safe_redirect( self::form_url( $id ) ); exit; } } /** * Edit links of products open PAYGRAB's form. * * @param string $link Link. * @param int $post_id Post. * @return string */ public static function edit_link( $link, $post_id ) { return Paygrab_Product::TYPE === get_post_type( $post_id ) ? self::form_url( $post_id ) : $link; } /** * Scripts of the list and of the form. * * @param string $hook Screen hook. * @return void */ public static function assets( $hook ) { $screen = get_current_screen(); if ( $screen && 'edit-' . Paygrab_Product::TYPE === $screen->id ) { wp_enqueue_script( 'paygrab-products', PAYGRAB_URL . 'assets/js/products-list.js', array(), PAYGRAB_VERSION, true ); wp_localize_script( 'paygrab-products', 'paygrabProducts', array( 'ajax' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'paygrab_toggle_sale' ), 'i18n' => array( 'copy' => __( 'Copy', 'paygrab' ), 'copied' => __( 'Copied ✓', 'paygrab' ), 'stop' => __( 'Stop selling', 'paygrab' ), 'resume' => __( 'Resume selling', 'paygrab' ), 'failed' => __( 'Could not change it. Reload the page and try again.', 'paygrab' ), ), ) ); return; } if ( ! str_ends_with( (string) $hook, '_page_' . self::FORM ) ) { return; } $asset = include PAYGRAB_PATH . 'assets/admin/product.asset.php'; wp_enqueue_script( 'paygrab-product-form', PAYGRAB_URL . 'assets/admin/product.js', $asset['dependencies'], $asset['version'], true ); wp_set_script_translations( 'paygrab-product-form', 'paygrab', PAYGRAB_PATH . 'languages' ); wp_enqueue_style( 'paygrab-product-form', PAYGRAB_URL . 'assets/admin/style-product.css', array( 'wp-components' ), $asset['version'] ); // The product image is chosen from the Media Library. wp_enqueue_media(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- navigation only. $id = isset( $_GET['product'] ) ? absint( $_GET['product'] ) : 0; if ( $id && Paygrab_Product::TYPE !== get_post_type( $id ) ) { $id = 0; } wp_add_inline_script( 'paygrab-product-form', 'window.paygrabProductBoot = ' . wp_json_encode( array( 'product' => $id ? self::payload( $id ) : null, 'defaults' => array( 'limit' => 5, 'days' => 30, ), 'maxSize' => wp_max_upload_size(), 'maxText' => size_format( wp_max_upload_size() ), 'exts' => Paygrab_Files::allowed_extensions(), 'payDays' => (int) Paygrab_Settings::get( 'pay_days' ), 'rest' => array( 'root' => esc_url_raw( rest_url() ), 'nonce' => wp_create_nonce( 'wp_rest' ), ), 'urls' => array( 'list' => self::list_url(), 'form' => self::form_url(), 'preview' => add_query_arg( array( Paygrab_Preview::ARG => 1, '_wpnonce' => wp_create_nonce( Paygrab_Preview::ARG ), ), home_url( '/' ) ), ), ) ) . ';', 'before' ); } /** * The form screen (React mounts here). * * @return void */ public static function render() { // Admin notices are placed after .wp-header-end, above the form instead of inside its header. echo '

' . esc_html__( 'Product', 'paygrab' ) . '


' . esc_html__( 'Loading…', 'paygrab' ) . '

'; } /** * Product as the form sees it. * * @param int $id Product. * @return array */ public static function payload( $id ) { $post = get_post( $id ); $data = Paygrab_Product::data( $id ); $file = $data['file'] && Paygrab_Files::path( $data['file']['path'] ) ? $data['file'] : null; $used = array(); foreach ( Paygrab_Product::used_in( $id ) as $p ) { $used[] = array( 'title' => get_the_title( $p ) ? get_the_title( $p ) : __( '(no title)', 'paygrab' ), 'url' => 'publish' === $p->post_status ? get_permalink( $p ) : get_edit_post_link( $p->ID, 'raw' ), 'draft' => 'publish' !== $p->post_status, ); } return array( 'id' => (int) $id, 'title' => $post ? $post->post_title : '', 'excerpt' => Paygrab_Product::excerpt( $post ), 'price' => $data['price'], 'limit' => $data['limit'], 'days' => $data['days'], 'onSale' => $data['on_sale'], 'file' => $file ? self::file_payload( $file ) : null, 'image' => self::image_payload( $data['image'] ), 'status' => $post ? $post->post_status : '', 'shortcode' => Paygrab_Product::shortcode_text( $id ), 'checkoutUrl' => Paygrab_Checkout::url( $id ), 'missing' => Paygrab_Product::missing( $id ), 'selling' => Paygrab_Product::purchasable( $id ), 'usedIn' => $used, ); } /** * The product image as the form sees it. * * @param int $image Attachment ID. * @return array{id:int,url:string}|null */ private static function image_payload( $image ) { $image = (int) $image; if ( ! $image || ! wp_attachment_is_image( $image ) ) { return null; } $src = wp_get_attachment_image_src( $image, 'medium' ); return array( 'id' => $image, 'url' => $src ? (string) $src[0] : '', ); } /** * A stored file as the form sees it. * * @param array $file Stored file. * @return array */ private static function file_payload( array $file ) { return array( 'name' => (string) $file['name'], 'size' => (int) $file['size'], /* translators: 1: file size, 2: upload date */ 'info' => sprintf( __( '%1$s · uploaded %2$s', 'paygrab' ), paygrab_size( $file['size'] ), wp_date( 'Y/m/d', (int) ( $file['time'] ?? time() ) ) ), ); } /** * REST routes of the form. * * @return void */ public static function routes() { $can = static function () { return current_user_can( self::CAP ); }; register_rest_route( 'paygrab/v1', '/products(?:/(?P\d+))?', array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'rest_save' ), 'permission_callback' => $can, ) ); register_rest_route( 'paygrab/v1', '/products/upload', array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'rest_upload' ), 'permission_callback' => $can, ) ); register_rest_route( 'paygrab/v1', '/products/(?P\d+)/trash', array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'rest_trash' ), 'permission_callback' => $can, ) ); } /** * Save a product (create when no ID). * * @param WP_REST_Request $request Request. * @return array|WP_Error */ public static function rest_save( WP_REST_Request $request ) { $id = (int) $request['id']; if ( $id && Paygrab_Product::TYPE !== get_post_type( $id ) ) { return new WP_Error( 'paygrab_not_found', __( 'This product could not be found.', 'paygrab' ), array( 'status' => 404 ) ); } if ( $id && 'trash' === get_post_status( $id ) ) { return new WP_Error( 'paygrab_in_trash', __( 'This product is in the Trash. Restore it from the Trash in the product list before editing.', 'paygrab' ), array( 'status' => 409 ) ); } // Whole numbers of 0 or more only; a minus sign is an error, not a positive number. $numbers = array(); foreach ( array( 'price' => 10000000, 'limit' => 1000, 'days' => 3650 ) as $key => $max ) { $raw = $request[ $key ]; // The price may be left empty for now: the product is then saved as stopped. if ( 'price' === $key && ( null === $raw || '' === $raw ) ) { $raw = '0'; } if ( null === $raw || '' === $raw || ! preg_match( '/^\d+$/', (string) $raw ) ) { return new WP_Error( 'paygrab_bad_number', /* translators: %s: field name */ sprintf( __( 'Enter a whole number of 0 or more for “%s”.', 'paygrab' ), self::number_label( $key ) ), array( 'status' => 400, 'field' => $key ) ); } $numbers[ $key ] = min( $max, (int) $raw ); } $title = trim( sanitize_text_field( (string) $request['title'] ) ); $excerpt = Paygrab_Product::clean_excerpt( (string) $request['excerpt'] ); // Take the uploaded file before creating anything, so a failure never leaves a half-made product. $new = null; if ( $request['fileToken'] ) { $new = Paygrab_Files::take( sanitize_text_field( (string) $request['fileToken'] ) ); if ( ! $new ) { return new WP_Error( 'paygrab_file_gone', __( 'The uploaded file could not be found. Please upload it again.', 'paygrab' ), array( 'status' => 400 ) ); } } $post = array( 'post_type' => Paygrab_Product::TYPE, 'post_status' => 'publish', 'post_title' => $title, 'post_excerpt' => $excerpt, ); if ( $id ) { $post['ID'] = $id; $result = wp_update_post( wp_slash( $post ), true ); } else { $result = wp_insert_post( wp_slash( $post ), true ); } if ( is_wp_error( $result ) ) { if ( $new ) { Paygrab_Files::delete( $new['path'] ); } return $result; } $id = (int) $result; update_post_meta( $id, '_paygrab_price', $numbers['price'] ); update_post_meta( $id, '_paygrab_download_limit', $numbers['limit'] ); update_post_meta( $id, '_paygrab_download_days', $numbers['days'] ); update_post_meta( $id, '_paygrab_on_sale', $request['onSale'] ? '1' : '0' ); // Product image (optional): an image in the Media Library, or 0 to remove it. // Left as it is when the request does not mention it (other callers of the REST route). if ( $request->has_param( 'image' ) ) { $image = absint( $request['image'] ); if ( $image && wp_attachment_is_image( $image ) ) { update_post_meta( $id, '_paygrab_image', $image ); } else { delete_post_meta( $id, '_paygrab_image' ); } } $old = get_post_meta( $id, '_paygrab_file', true ); if ( $new ) { update_post_meta( $id, '_paygrab_file', $new ); } elseif ( $request['removeFile'] ) { delete_post_meta( $id, '_paygrab_file' ); } if ( ( $new || $request['removeFile'] ) && is_array( $old ) && ! empty( $old['path'] ) && ( ! $new || $new['path'] !== $old['path'] ) && ! Paygrab_Files::in_use( $old['path'], $id ) ) { Paygrab_Files::delete( $old['path'] ); } /** * Fires after a product was saved from the form (PAYGRAB Pro saves its fields here). * * @param int $id Product. * @param WP_REST_Request $request Values sent by the form. */ do_action( 'paygrab_product_saved', $id, $request ); return self::payload( $id ); } /** * Name of a number field, for error messages. * * @param string $key price | limit | days. * @return string */ private static function number_label( $key ) { $labels = array( 'price' => __( 'Price (tax included)', 'paygrab' ), 'limit' => __( 'Count', 'paygrab' ), 'days' => __( 'Period', 'paygrab' ), ); return $labels[ $key ] ?? $key; } /** * Upload a file from the form. It is held until the product is saved. * * @param WP_REST_Request $request Request. * @return array|WP_Error */ public static function rest_upload( WP_REST_Request $request ) { $files = $request->get_file_params(); if ( empty( $files['file'] ) || ! is_array( $files['file'] ) ) { return new WP_Error( 'paygrab_no_file', __( 'Choose a file.', 'paygrab' ), array( 'status' => 400 ) ); } $stored = Paygrab_Files::store_upload( $files['file'] ); if ( is_wp_error( $stored ) ) { $stored->add_data( array( 'status' => 400 ) ); return $stored; } return array( 'token' => Paygrab_Files::hold( $stored ), 'file' => self::file_payload( $stored ), ); } /** * Move a product to the Trash. * * @param WP_REST_Request $request Request. * @return array|WP_Error */ public static function rest_trash( WP_REST_Request $request ) { $id = (int) $request['id']; if ( Paygrab_Product::TYPE !== get_post_type( $id ) || ! wp_trash_post( $id ) ) { return new WP_Error( 'paygrab_not_found', __( 'This product could not be found.', 'paygrab' ), array( 'status' => 404 ) ); } return array( 'redirect' => add_query_arg( array( 'trashed' => 1, 'ids' => $id ), self::list_url() ) ); } /** * List columns. * * @param array $columns Columns. * @return array */ public static function columns( $columns ) { return array( 'cb' => $columns['cb'] ?? '', 'paygrab_image' => '' . esc_html__( 'Product image', 'paygrab' ) . '', 'title' => __( 'Product name', 'paygrab' ), 'paygrab_price' => __( 'Price (tax incl.)', 'paygrab' ), 'paygrab_file' => __( 'File', 'paygrab' ), 'paygrab_shortcode' => __( 'Shortcode', 'paygrab' ), 'paygrab_sale' => __( 'Sale', 'paygrab' ), 'paygrab_sold' => __( 'Sold', 'paygrab' ), 'paygrab_updated' => __( 'Updated', 'paygrab' ), ); } /** * Column values. * * @param string $column Column. * @param int $post_id Product. * @return void */ public static function column( $column, $post_id ) { static $counts = null; $data = Paygrab_Product::data( $post_id ); switch ( $column ) { case 'paygrab_image': // A small thumbnail so products are easy to tell apart (a grey square when there is none). $thumb = $data['image'] && wp_attachment_is_image( $data['image'] ) ? wp_get_attachment_image( $data['image'], array( 96, 96 ), false, array( 'alt' => '', 'class' => 'paygrab-thumb' ) ) : ''; echo $thumb ? $thumb : ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- built by wp_get_attachment_image(). break; case 'paygrab_price': echo $data['price'] ? esc_html( paygrab_price( $data['price'] ) ) : '' . esc_html__( 'Not set', 'paygrab' ) . ''; break; case 'paygrab_file': if ( $data['file'] && Paygrab_Files::path( $data['file']['path'] ) ) { echo esc_html( $data['file']['name'] ) . '
' . esc_html( paygrab_size( $data['file']['size'] ) ) . ''; } else { echo '' . esc_html__( 'No file', 'paygrab' ) . ''; } break; case 'paygrab_shortcode': $code = Paygrab_Product::shortcode_text( $post_id ); echo '
' . esc_html( $code ) . '
'; break; case 'paygrab_sale': echo self::sale_html( $post_id ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- built from escaped parts. break; case 'paygrab_sold': if ( null === $counts ) { global $wp_query; $counts = Paygrab_Orders::product_counts( wp_list_pluck( (array) $wp_query->posts, 'ID' ) ); } echo esc_html( (string) ( $counts[ $post_id ]['paid'] ?? 0 ) ); if ( ! empty( $counts[ $post_id ]['pending'] ) ) { /* translators: %d: number of orders */ echo '
' . esc_html( sprintf( __( '%d awaiting payment', 'paygrab' ), $counts[ $post_id ]['pending'] ) ) . ''; } break; case 'paygrab_updated': echo esc_html( get_the_modified_date( 'Y/m/d', $post_id ) ); break; } } /** * In the list's extended view, keep the short description to about two lines (the full text is in the editor). * * @param string $excerpt Excerpt. * @param WP_Post|null $post Post. * @return string */ public static function list_excerpt( $excerpt, $post = null ) { // get_current_screen() exists only in the admin (and in WP-CLI once the screen files are loaded). if ( ! $post instanceof WP_Post || Paygrab_Product::TYPE !== $post->post_type || ! function_exists( 'get_current_screen' ) ) { return $excerpt; } $screen = get_current_screen(); if ( ! $screen || 'edit-' . Paygrab_Product::TYPE !== $screen->id ) { return $excerpt; } return mb_strlen( $excerpt ) > 90 ? mb_substr( $excerpt, 0, 89 ) . '…' : $excerpt; } /** * Sale status of a product, with what is missing. * * @param int $post_id Product. * @return string HTML. */ public static function sale_html( $post_id ) { if ( 'trash' === get_post_status( $post_id ) ) { return '' . esc_html__( 'In the Trash', 'paygrab' ) . ''; } if ( Paygrab_Product::purchasable( $post_id ) ) { return '' . esc_html__( 'On sale', 'paygrab' ) . ''; } $html = '' . esc_html__( 'Stopped', 'paygrab' ) . ''; $missing = Paygrab_Product::missing( $post_id ); if ( $missing ) { $labels = array_intersect_key( Paygrab_Product::missing_labels(), array_flip( $missing ) ); /* translators: %s: missing items, e.g. "file" */ $html .= '
' . esc_html( sprintf( __( 'Add the %s to sell it', 'paygrab' ), implode( wp_get_list_item_separator(), $labels ) ) ) . ''; } return $html; } /** * Row actions: Edit, Stop / Resume selling, Duplicate, Trash. * * @param array $actions Actions. * @param WP_Post $post Post. * @return array */ public static function row_actions( $actions, $post ) { if ( Paygrab_Product::TYPE !== $post->post_type || 'trash' === $post->post_status ) { return $actions; } $on = Paygrab_Product::data( $post->ID )['on_sale']; $new = array(); $new['edit'] = '' . esc_html__( 'Edit', 'paygrab' ) . ''; // A product that is missing its name, price or file cannot be sold either way: the Sale column says what to add. if ( ! Paygrab_Product::missing( $post->ID ) ) { $new['paygrab_sale'] = '' . ( $on ? esc_html__( 'Stop selling', 'paygrab' ) : esc_html__( 'Resume selling', 'paygrab' ) ) . ''; } $new['paygrab_dup'] = '' . esc_html__( 'Duplicate', 'paygrab' ) . ''; if ( isset( $actions['trash'] ) ) { $new['trash'] = $actions['trash']; } return $new; } /** * Views: All | On sale | Stopped. * * @param array $views Views. * @return array */ public static function views( $views ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- navigation only. $current = isset( $_GET['paygrab_sale'] ) ? sanitize_key( wp_unslash( $_GET['paygrab_sale'] ) ) : ''; $on = count( self::selling_ids() ); $all = count( self::published_ids() ); $items = array( 'on' => array( __( 'On sale', 'paygrab' ), $on ), 'off' => array( __( 'Stopped', 'paygrab' ), max( 0, $all - $on ) ), ); $out = array(); foreach ( $views as $key => $html ) { if ( 'all' === $key ) { $out['all'] = $current ? str_replace( array( ' class="current"', 'aria-current="page"' ), '', $html ) : $html; foreach ( $items as $view => $item ) { $url = add_query_arg( array( 'post_type' => Paygrab_Product::TYPE, 'paygrab_sale' => $view ), admin_url( 'edit.php' ) ); $attrs = $current === $view ? ' class="current" aria-current="page"' : ''; $out[ $view ] = sprintf( '%s (%d)', esc_url( $url ), $attrs, esc_html( $item[0] ), $item[1] ); } continue; } if ( 'trash' === $key ) { $out[ $key ] = $html; } } return $out; } /** * Published products. * * @return int[] */ private static function published_ids() { return array_map( 'intval', get_posts( array( 'post_type' => Paygrab_Product::TYPE, 'post_status' => 'publish', 'numberposts' => -1, 'fields' => 'ids', ) ) ); } /** * Products that can be bought now (what the Sale column calls "On sale"). * * @return int[] */ public static function selling_ids() { // The list asks several times per page (counts and the filter); work it out once. static $ids = null; if ( null === $ids || ( defined( 'WP_CLI' ) && WP_CLI ) ) { $ids = array_values( array_filter( self::published_ids(), array( 'Paygrab_Product', 'purchasable' ) ) ); } return $ids; } /** * Apply the On sale / Stopped view. * * @param WP_Query $query Query. * @return void */ public static function filter_list( $query ) { if ( ! is_admin() || ! $query->is_main_query() || Paygrab_Product::TYPE !== $query->get( 'post_type' ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- navigation only. $view = isset( $_GET['paygrab_sale'] ) ? sanitize_key( wp_unslash( $_GET['paygrab_sale'] ) ) : ''; // Same rule as the Sale column: "on sale" means it can be bought now. if ( 'on' === $view ) { $ids = self::selling_ids(); $query->set( 'post__in', $ids ? $ids : array( 0 ) ); $query->set( 'post_status', 'publish' ); } elseif ( 'off' === $view ) { $ids = array_values( array_diff( self::published_ids(), self::selling_ids() ) ); $query->set( 'post__in', $ids ? $ids : array( 0 ) ); $query->set( 'post_status', 'publish' ); } if ( ! $query->get( 'orderby' ) ) { $query->set( 'orderby', 'modified' ); $query->set( 'order', 'DESC' ); } } /** * Bulk actions: only the Trash ones (no editing in the block editor). * * @param array $actions Actions. * @return array */ public static function bulk_actions( $actions ) { return array_intersect_key( $actions, array_flip( array( 'trash', 'untrash', 'delete' ) ) ); } /** * Messages after moving products to and from the Trash. * * @param array $messages Messages by post type. * @param array $counts Counts by action. * @return array */ public static function bulk_messages( $messages, $counts ) { $messages[ Paygrab_Product::TYPE ] = array( /* translators: %s: number of products */ 'updated' => _n( '%s product updated.', '%s products updated.', $counts['updated'], 'paygrab' ), /* translators: %s: number of products */ 'locked' => _n( '%s product not updated, somebody is editing it.', '%s products not updated, somebody is editing them.', $counts['locked'], 'paygrab' ), /* translators: %s: number of products */ 'deleted' => _n( '%s product permanently deleted.', '%s products permanently deleted.', $counts['deleted'], 'paygrab' ), /* translators: %s: number of products */ 'trashed' => _n( '%s product moved to the Trash. People who bought it can still download it.', '%s products moved to the Trash. People who bought them can still download them.', $counts['trashed'], 'paygrab' ), /* translators: %s: number of products */ 'untrashed' => _n( '%s product restored from the Trash.', '%s products restored from the Trash.', $counts['untrashed'], 'paygrab' ), ); return $messages; } /** * No month filter on the product list. * * @param bool $disable Disable. * @param string $post_type Post type. * @return bool */ public static function no_months( $disable, $post_type ) { return Paygrab_Product::TYPE === $post_type ? true : $disable; } /** * Whether there is no product at all yet (not even one in the Trash). * * @return bool */ private static function no_products() { $counts = wp_count_posts( Paygrab_Product::TYPE ); $total = 0; foreach ( (array) $counts as $status => $n ) { if ( ! in_array( $status, array( 'trash', 'auto-draft' ), true ) ) { $total += (int) $n; } } return ! $total && empty( $counts->trash ); } /** * Body class that hides the empty list table while the "add your first product" box is shown (see admin.css). * * @param string $classes Space separated classes. * @return string */ public static function body_class( $classes ) { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( $screen && 'edit-' . Paygrab_Product::TYPE === $screen->id && self::no_products() ) { $classes .= ' paygrab-no-products'; } return $classes; } /** * Hint above the list, or the first-product screen when there are no products. * * @return void */ public static function list_notice() { $screen = get_current_screen(); if ( ! $screen || 'edit-' . Paygrab_Product::TYPE !== $screen->id ) { return; } if ( self::no_products() ) { echo '

' . esc_html__( 'Let’s add your first product', 'paygrab' ) . '

' . esc_html__( 'Register the name, price and ZIP file. You get a shortcode to paste into any post or page; the price and the purchase button appear there.', 'paygrab' ) . '

' . esc_html__( 'Add new product', 'paygrab' ) . '
'; return; } echo '

' . wp_kses( __( 'Products have no page of their own. Copy the shortcode and paste it into a blog post or page. The price and the purchase button (buy box) appear there. You can paste any number of them into one page.', 'paygrab' ), array( 'strong' => array() ) ) . '

'; } /** * Stop or resume selling from the list. * * @return never */ public static function ajax_toggle_sale() { check_ajax_referer( 'paygrab_toggle_sale' ); $id = isset( $_POST['id'] ) ? absint( $_POST['id'] ) : 0; if ( ! current_user_can( self::CAP ) || Paygrab_Product::TYPE !== get_post_type( $id ) ) { wp_send_json_error( null, 403 ); } $on = ! empty( $_POST['on'] ); update_post_meta( $id, '_paygrab_on_sale', $on ? '1' : '0' ); wp_update_post( array( 'ID' => $id ) ); wp_send_json_success( array( 'on' => $on, 'sale' => self::sale_html( $id ), ) ); } /** * Duplicate a product without its file. The copy starts stopped. * * @return never */ public static function duplicate() { $id = isset( $_GET['product'] ) ? absint( $_GET['product'] ) : 0; check_admin_referer( 'paygrab_duplicate_' . $id ); if ( ! current_user_can( self::CAP ) || Paygrab_Product::TYPE !== get_post_type( $id ) ) { wp_die( esc_html__( 'You are not allowed to do this.', 'paygrab' ), 403 ); } $post = get_post( $id ); $copy = wp_insert_post( wp_slash( array( 'post_type' => Paygrab_Product::TYPE, 'post_status' => 'publish', /* translators: %s: product name */ 'post_title' => sprintf( __( '%s (copy)', 'paygrab' ), $post->post_title ), 'post_excerpt' => $post->post_excerpt, ) ), true ); if ( is_wp_error( $copy ) ) { wp_die( esc_html( $copy->get_error_message() ) ); } foreach ( array( '_paygrab_price', '_paygrab_download_limit', '_paygrab_download_days', '_paygrab_image' ) as $key ) { if ( metadata_exists( 'post', $id, $key ) ) { update_post_meta( $copy, $key, get_post_meta( $id, $key, true ) ); } } update_post_meta( $copy, '_paygrab_on_sale', '0' ); /** * Fires after a product was duplicated (PAYGRAB Pro copies its fields here). * * @param int $copy New product. * @param int $id Original product. */ do_action( 'paygrab_product_duplicated', (int) $copy, $id ); wp_safe_redirect( add_query_arg( 'duplicated', 1, self::form_url( (int) $copy ) ) ); exit; } }