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' ) . '
' . 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;
}
}