> */ static function definitions() { if (!\ONEMCP\Detector::is_active('edd')) { return array(); } $abilities = array( Helper::ability( 'list-downloads', __('List downloads', 'onemcp-pro'), __('Lists Easy Digital Downloads products.', 'onemcp-pro'), 'onemcp-pro-edd', true, array(__CLASS__, 'list_downloads'), array( 'search' => Helper::string_prop(__('Search titles.', 'onemcp-pro'), 120), 'status' => Helper::string_prop(__('publish, draft, pending, private, or any.', 'onemcp-pro'), 20), 'limit' => Helper::int_prop(__('How many downloads to return. Maximum 50.', 'onemcp-pro')), 'page' => Helper::int_prop(__('Page number, starting at 1.', 'onemcp-pro')), ), array(), array('permission' => array(__CLASS__, 'can_edit_downloads')) ), Helper::ability( 'get-download', __('Get download', 'onemcp-pro'), __('Returns one download, including prices, files, and the download limit.', 'onemcp-pro'), 'onemcp-pro-edd', true, array(__CLASS__, 'get_download'), array('id' => Helper::int_prop(__('Download ID.', 'onemcp-pro'))), array('id'), array('permission' => array(__CLASS__, 'can_edit_downloads')) ), Helper::ability( 'create-download', __('Create download', 'onemcp-pro'), __('Creates an Easy Digital Downloads product. New downloads are drafts unless a status is given.', 'onemcp-pro'), 'onemcp-pro-edd', false, array(__CLASS__, 'create_download'), array( 'title' => Helper::string_prop(__('Download title.', 'onemcp-pro'), 200), 'content' => Helper::string_prop(__('Description HTML.', 'onemcp-pro'), 50000), 'status' => Helper::string_prop(__('draft, pending, private, or publish.', 'onemcp-pro'), 20), 'price' => Helper::string_prop(__('Simple price. Ignored when prices is set.', 'onemcp-pro'), 20), 'prices' => self::variable_price_prop(), 'files' => self::download_file_prop(), 'download_limit' => Helper::int_prop(__('Maximum downloads per customer. 0 is unlimited. -1 uses the store default.', 'onemcp-pro')), ), array('title'), array('permission' => array(__CLASS__, 'can_edit_downloads')) ), Helper::ability( 'update-download', __('Update download', 'onemcp-pro'), __('Updates the title, content, status, simple price, variable prices, files, or download limit of one download.', 'onemcp-pro'), 'onemcp-pro-edd', false, array(__CLASS__, 'update_download'), array( 'id' => Helper::int_prop(__('Download ID.', 'onemcp-pro')), 'title' => Helper::string_prop(__('New title.', 'onemcp-pro'), 200), 'content' => Helper::string_prop(__('New description HTML.', 'onemcp-pro'), 50000), 'status' => Helper::string_prop(__('draft, pending, private, or publish.', 'onemcp-pro'), 20), 'price' => Helper::string_prop(__('Simple price. Ignored when prices is set.', 'onemcp-pro'), 20), 'prices' => self::variable_price_prop(), 'files' => self::download_file_prop(), 'download_limit' => Helper::int_prop(__('Maximum downloads per customer. 0 is unlimited. -1 uses the store default.', 'onemcp-pro')), ), array('id'), array('permission' => array(__CLASS__, 'can_edit_downloads')) ), Helper::ability( 'list-edd-orders', __('List EDD orders', 'onemcp-pro'), __('Lists Easy Digital Downloads orders. Search by email or order number, or filter by customer.', 'onemcp-pro'), 'onemcp-pro-edd', true, array(__CLASS__, 'list_orders'), array( 'status' => Helper::string_prop(__('Order status, such as complete or pending.', 'onemcp-pro'), 30), 'search' => Helper::string_prop(__('Search by email or order number.', 'onemcp-pro'), 120), 'customer_id' => Helper::int_prop(__('Limit to one EDD customer ID.', 'onemcp-pro')), 'limit' => Helper::int_prop(__('How many orders to return. Maximum 50.', 'onemcp-pro')), 'page' => Helper::int_prop(__('Page number, starting at 1.', 'onemcp-pro')), ), array(), array('permission' => array(__CLASS__, 'can_manage_orders')) ), Helper::ability( 'get-edd-order', __('Get EDD order', 'onemcp-pro'), __('Returns one Easy Digital Downloads order, including purchased items.', 'onemcp-pro'), 'onemcp-pro-edd', true, array(__CLASS__, 'get_order'), array('id' => Helper::int_prop(__('Order ID.', 'onemcp-pro'))), array('id'), array('permission' => array(__CLASS__, 'can_manage_orders')) ), Helper::ability( 'update-edd-order-status', __('Update EDD order status', 'onemcp-pro'), __('Changes the status of one Easy Digital Downloads order.', 'onemcp-pro'), 'onemcp-pro-edd', false, array(__CLASS__, 'update_order_status'), array( 'id' => Helper::int_prop(__('Order ID.', 'onemcp-pro')), 'status' => Helper::string_prop(__('complete, pending, processing, refunded, partially_refunded, failed, abandoned, revoked, or on_hold.', 'onemcp-pro'), 30), ), array('id', 'status'), array('permission' => array(__CLASS__, 'can_manage_orders')) ), Helper::ability( 'list-discounts', __('List discounts', 'onemcp-pro'), __('Lists discount codes, including limits, dates, and product restrictions.', 'onemcp-pro'), 'onemcp-pro-edd', true, array(__CLASS__, 'list_discounts'), array( 'limit' => Helper::int_prop(__('How many discounts to return. Maximum 50.', 'onemcp-pro')), 'page' => Helper::int_prop(__('Page number, starting at 1.', 'onemcp-pro')), ), array(), array('permission' => array(__CLASS__, 'can_manage_orders')) ), Helper::ability( 'create-discount', __('Create discount', 'onemcp-pro'), __('Creates an Easy Digital Downloads discount code. New codes are active unless status is inactive.', 'onemcp-pro'), 'onemcp-pro-edd', false, array(__CLASS__, 'create_discount'), self::discount_properties(), array('code', 'amount', 'amount_type'), array('permission' => array(__CLASS__, 'can_manage_orders')) ), Helper::ability( 'update-discount', __('Update discount', 'onemcp-pro'), __('Updates the code, amount, dates, limits, or product restrictions of one discount.', 'onemcp-pro'), 'onemcp-pro-edd', false, array(__CLASS__, 'update_discount'), self::discount_properties() + array( 'id' => Helper::int_prop(__('Discount ID.', 'onemcp-pro')), ), array('id'), array('permission' => array(__CLASS__, 'can_manage_orders')) ), Helper::ability( 'update-download-link-expiration', __('Update download link expiration', 'onemcp-pro'), __('Sets how many hours a download link stays valid. This is a store-wide setting. 0 means links do not expire.', 'onemcp-pro'), 'onemcp-pro-edd', false, array(__CLASS__, 'update_link_expiration'), array( 'hours' => Helper::int_prop(__('Hours before a download link expires. 0 to 8760. 0 means never.', 'onemcp-pro')), ), array('hours'), array('permission' => array(__CLASS__, 'can_manage_settings')) ), Helper::ability( 'update-edd-order', __('Update EDD order', 'onemcp-pro'), __('Updates the customer email or status of one order. Totals, gateway, and payment key stay unchanged.', 'onemcp-pro'), 'onemcp-pro-edd', false, array(__CLASS__, 'update_order'), array( 'id' => Helper::int_prop(__('Order ID.', 'onemcp-pro')), 'email' => Helper::string_prop(__('Customer email.', 'onemcp-pro'), 190), 'status' => Helper::string_prop(__('complete, pending, processing, refunded, partially_refunded, failed, abandoned, revoked, or on_hold.', 'onemcp-pro'), 30), ), array('id'), array('permission' => array(__CLASS__, 'can_manage_orders')) ), Helper::ability( 'add-edd-order-note', __('Add EDD order note', 'onemcp-pro'), __('Adds a note to one Easy Digital Downloads order.', 'onemcp-pro'), 'onemcp-pro-edd', false, array(__CLASS__, 'add_order_note'), array( 'id' => Helper::int_prop(__('Order ID.', 'onemcp-pro')), 'content' => Helper::string_prop(__('Note text.', 'onemcp-pro'), 5000), ), array('id', 'content'), array('permission' => array(__CLASS__, 'can_manage_orders')) ), Helper::ability( 'list-edd-customers', __('List EDD customers', 'onemcp-pro'), __('Lists Easy Digital Downloads customers.', 'onemcp-pro'), 'onemcp-pro-edd', true, array(__CLASS__, 'list_customers'), array( 'search' => Helper::string_prop(__('Search name or email.', 'onemcp-pro'), 120), 'limit' => Helper::int_prop(__('How many customers to return. Maximum 50.', 'onemcp-pro')), 'page' => Helper::int_prop(__('Page number, starting at 1.', 'onemcp-pro')), ), array(), array('permission' => array(__CLASS__, 'can_manage_orders')) ), Helper::ability( 'get-edd-customer', __('Get EDD customer', 'onemcp-pro'), __('Returns one customer, including recent purchases and download history.', 'onemcp-pro'), 'onemcp-pro-edd', true, array(__CLASS__, 'get_customer'), array( 'id' => Helper::int_prop(__('Customer ID.', 'onemcp-pro')), 'email' => Helper::string_prop(__('Customer email. Used when id is omitted.', 'onemcp-pro'), 190), ), array(), array('permission' => array(__CLASS__, 'can_manage_orders')) ), Helper::ability( 'get-edd-sales-summary', __('Get EDD sales summary', 'onemcp-pro'), __('Shows earnings, order count, and best-selling downloads. Best sellers use at most the latest 100 matching orders.', 'onemcp-pro'), 'onemcp-pro-edd', true, array(__CLASS__, 'sales_summary'), array( 'days' => Helper::int_prop(__('How many days to include. 1 to 365. Defaults to 30.', 'onemcp-pro')), ), array(), array('permission' => array(__CLASS__, 'can_manage_orders')) ), Helper::ability( 'list-download-logs', __('List download logs', 'onemcp-pro'), __('Lists file download activity so you can investigate access. Includes the customer IP address.', 'onemcp-pro'), 'onemcp-pro-edd', true, array(__CLASS__, 'list_download_logs'), array( 'product_id' => Helper::int_prop(__('Limit to one download ID.', 'onemcp-pro')), 'customer_id' => Helper::int_prop(__('Limit to one customer ID.', 'onemcp-pro')), 'order_id' => Helper::int_prop(__('Limit to one order ID.', 'onemcp-pro')), 'email' => Helper::string_prop(__('Limit to one customer email.', 'onemcp-pro'), 190), 'limit' => Helper::int_prop(__('How many logs to return. Maximum 50.', 'onemcp-pro')), 'page' => Helper::int_prop(__('Page number, starting at 1.', 'onemcp-pro')), ), array(), array('permission' => array(__CLASS__, 'can_manage_orders')) ), Helper::ability( 'set-download-access', __('Set download access', 'onemcp-pro'), __('Grants or revokes download access. Grant without an order creates a manual zero-total completed order and does not email a receipt. Set confirm to GRANT or REVOKE.', 'onemcp-pro'), 'onemcp-pro-edd', false, array(__CLASS__, 'set_download_access'), array( 'action' => Helper::string_prop(__('grant or revoke.', 'onemcp-pro'), 20), 'confirm' => Helper::string_prop(__('GRANT when granting, or REVOKE when revoking.', 'onemcp-pro'), 20), 'order_id' => Helper::int_prop(__('Existing order ID. Required to revoke.', 'onemcp-pro')), 'order_item_id' => Helper::int_prop(__('Order item ID to update.', 'onemcp-pro')), 'customer_id' => Helper::int_prop(__('Customer ID. Used when granting without an order.', 'onemcp-pro')), 'email' => Helper::string_prop(__('Customer email. Used when granting without an order or customer id.', 'onemcp-pro'), 190), 'download_id' => Helper::int_prop(__('Download ID.', 'onemcp-pro')), 'price_id' => Helper::int_prop(__('Variable price index. Required when the download has variable prices.', 'onemcp-pro')), ), array('action', 'confirm'), array('permission' => array(__CLASS__, 'can_manage_orders'), 'destructive' => true) ), ); return array_merge($abilities, self::extension_abilities()); } /** * Shop staff who can edit downloads. * * @return bool */ static function can_edit_downloads() { return current_user_can('edit_products') || current_user_can('manage_options'); } /** * Shop staff who can manage orders. * * @return bool */ static function can_manage_orders() { return current_user_can('edit_shop_payments') || current_user_can('manage_options'); } /** * Shop staff who can change store settings. * * @return bool */ static function can_manage_settings() { return current_user_can('manage_shop_settings') || current_user_can('manage_options'); } /** * List downloads. * * @param array $input Input. * @return array|WP_Error */ static function list_downloads($input) { if (!post_type_exists('download')) { return self::missing(); } $status = isset($input['status']) ? sanitize_key((string) $input['status']) : 'any'; if (!in_array($status, array('any', 'publish', 'draft', 'pending', 'private'), true)) { $status = 'any'; } $limit = Helper::limit(isset($input['limit']) ? $input['limit'] : 20, 20, 50); $page = Helper::page($input); $args = array( 'post_type' => 'download', 'post_status' => $status, 'posts_per_page' => $limit, 'paged' => $page, 'orderby' => 'modified', 'order' => 'DESC', ); if (!empty($input['search'])) { $args['s'] = Helper::text($input['search'], 120); } $query = Helper::query_posts($args); $items = array(); foreach ($query->posts as $post) { if ($post instanceof WP_Post) { $items[] = self::download_row($post, false); } } return array( 'items' => $items, 'total' => (int) $query->found_posts, 'page' => $page, ); } /** * One download. * * @param array $input Input. * @return array|WP_Error */ static function get_download($input) { $post = self::download(isset($input['id']) ? $input['id'] : 0); if (is_wp_error($post)) { return $post; } return self::download_row($post, true); } /** * Create a download. * * @param array $input Input. * @return array|WP_Error */ static function create_download($input) { if (!post_type_exists('download')) { return self::missing(); } $title = Helper::text(isset($input['title']) ? $input['title'] : '', 200); if ($title === '') { return Helper::error('onemcp-pro_invalid', __('Provide a download title.', 'onemcp-pro')); } if (!Helper::user_can_create('download')) { return Helper::error('onemcp-pro_forbidden', __('You cannot create downloads.', 'onemcp-pro')); } $post_id = wp_insert_post(wp_slash(array( 'post_type' => 'download', 'post_title' => $title, 'post_content' => isset($input['content']) ? wp_kses_post((string) $input['content']) : '', 'post_status' => Helper::post_status(isset($input['status']) ? $input['status'] : 'draft', 'download', 'draft'), )), true); if (is_wp_error($post_id)) { return $post_id; } $commerce = self::apply_download_commerce($post_id, $input); if (is_wp_error($commerce)) { wp_delete_post($post_id, true); return $commerce; } $post = get_post($post_id); return $post instanceof WP_Post ? self::download_row($post, true) : array('id' => (int) $post_id); } /** * Update a download. * * @param array $input Input. * @return array|WP_Error */ static function update_download($input) { $post = self::download(isset($input['id']) ? $input['id'] : 0); if (is_wp_error($post)) { return $post; } $update = array('ID' => $post->ID); if (array_key_exists('title', $input)) { $update['post_title'] = Helper::text($input['title'], 200); } if (array_key_exists('content', $input)) { $update['post_content'] = wp_kses_post((string) $input['content']); } if (array_key_exists('status', $input)) { $update['post_status'] = Helper::post_status($input['status'], 'download', $post->post_status); } if (count($update) > 1) { $result = wp_update_post(wp_slash($update), true); if (is_wp_error($result)) { return $result; } } $commerce = self::apply_download_commerce($post->ID, $input); if (is_wp_error($commerce)) { return $commerce; } if (count($update) === 1 && !$commerce) { return Helper::error('onemcp-pro_invalid', __('Provide a title, content, status, price, prices, files, or download limit.', 'onemcp-pro')); } $fresh = get_post($post->ID); return $fresh instanceof WP_Post ? self::download_row($fresh, true) : array('id' => (int) $post->ID); } /** * List orders. * * @param array $input Input. * @return array|WP_Error */ static function list_orders($input) { if (!function_exists('edd_get_orders')) { return Helper::error('onemcp-pro_unavailable', __('This Easy Digital Downloads version does not expose edd_get_orders.', 'onemcp-pro')); } $limit = Helper::limit(isset($input['limit']) ? $input['limit'] : 20, 20, 50); $page = Helper::page($input); $args = array( 'number' => $limit, 'offset' => ($page - 1) * $limit, 'orderby' => 'date_created', 'order' => 'DESC', ); if (!empty($input['status'])) { $status = self::order_status($input['status']); if (is_wp_error($status)) { return $status; } $args['status'] = $status; } $customer_id = isset($input['customer_id']) ? Helper::absint($input['customer_id']) : 0; if ($customer_id > 0) { $args['customer_id'] = $customer_id; } if (!empty($input['search'])) { $search = Helper::text($input['search'], 120); if (is_email($search)) { $args['email'] = $search; } else { $args['search'] = $search; } } $orders = edd_get_orders($args); $items = array(); foreach ((array) $orders as $order) { $row = self::order_row($order); if ($row !== array()) { $items[] = $row; } } return array( 'items' => $items, 'page' => $page, ); } /** * One order. * * @param array $input Input. * @return array|WP_Error */ static function get_order($input) { $order = self::order(isset($input['id']) ? $input['id'] : 0); if (is_wp_error($order)) { return $order; } return self::order_row($order, true); } /** * Change an order status. * * @param array $input Input. * @return array|WP_Error */ static function update_order_status($input) { if (!function_exists('edd_update_order_status')) { return Helper::error('onemcp-pro_unavailable', __('This Easy Digital Downloads version cannot update order status.', 'onemcp-pro')); } $order = self::order(isset($input['id']) ? $input['id'] : 0); if (is_wp_error($order)) { return $order; } $status = self::order_status(isset($input['status']) ? $input['status'] : ''); if (is_wp_error($status)) { return $status; } $updated = edd_update_order_status($order->id, $status); if (!$updated) { return Helper::error('onemcp-pro_failed', __('The order status was not changed.', 'onemcp-pro')); } return self::get_order(array('id' => $order->id)); } /** * Discount codes. * * @param array $input Input. * @return array|WP_Error */ static function list_discounts($input) { if (!function_exists('edd_get_discounts')) { return Helper::error('onemcp-pro_unavailable', __('This Easy Digital Downloads version does not expose discounts.', 'onemcp-pro')); } $limit = Helper::limit(isset($input['limit']) ? $input['limit'] : 20, 20, 50); $page = Helper::page($input); $discounts = edd_get_discounts(array( 'number' => $limit, 'offset' => ($page - 1) * $limit, )); $items = array(); foreach ((array) $discounts as $discount) { $row = self::discount_row($discount); if ($row !== array()) { $items[] = $row; } } return array( 'items' => $items, 'page' => $page, ); } /** * Download payload. * * @param WP_Post $post Download. * @param bool $full Include content. * @return array */ private static function download_row($post, $full) { $price = get_post_meta($post->ID, 'edd_price', true); $variable = get_post_meta($post->ID, 'edd_variable_prices', true); if ($price === '' && function_exists('edd_get_download_price')) { $price = edd_get_download_price($post->ID); } $row = array( 'id' => (int) $post->ID, 'title' => html_entity_decode(get_the_title($post), ENT_QUOTES, 'UTF-8'), 'status' => $post->post_status, 'slug' => $post->post_name, 'price' => is_scalar($price) ? (string) $price : '', 'variable_prices' => is_array($variable) && $variable !== array(), 'link' => get_permalink($post), ); if ($full) { $row['content'] = Helper::trim_text($post->post_content, 20000); $row['prices'] = self::price_rows($post->ID, is_array($variable) ? $variable : array()); $row['files'] = self::file_rows($post->ID); $stored_limit = get_post_meta($post->ID, '_edd_download_limit', true); $row['download_limit'] = ($stored_limit === '' || false === $stored_limit) ? null : (int) $stored_limit; $row['download_limit_effective'] = self::effective_download_limit($post->ID); $row['link_expiration_hours'] = self::link_expiration_hours(); } return $row; } /** * Public order fields. Payment keys are omitted. * * @param mixed $order Order object. * @param bool $full Include purchased items. * @return array */ private static function order_row($order, $full = false) { if (!is_object($order)) { return array(); } $row = array( 'id' => isset($order->id) ? (int) $order->id : 0, 'number' => isset($order->order_number) ? (string) $order->order_number : '', 'status' => isset($order->status) ? (string) $order->status : '', 'total' => isset($order->total) ? (string) $order->total : '', 'currency' => isset($order->currency) ? (string) $order->currency : '', 'email' => isset($order->email) ? (string) $order->email : '', 'customer_id' => isset($order->customer_id) ? (int) $order->customer_id : 0, 'date' => isset($order->date_created) ? (string) $order->date_created : '', ); if ($full) { $row['items'] = self::order_item_rows(isset($order->id) ? (int) $order->id : 0); } return $row; } /** * A download the current user can edit. * * @param mixed $id Download ID. * @return WP_Post|WP_Error */ private static function download($id) { $post = get_post(Helper::absint($id)); if (!$post instanceof WP_Post || $post->post_type !== 'download') { return Helper::error('onemcp-pro_not_found', __('Download was not found.', 'onemcp-pro')); } if (!current_user_can('edit_post', $post->ID)) { return Helper::error('onemcp-pro_forbidden', __('You cannot edit this download.', 'onemcp-pro')); } return $post; } /** * One EDD order. * * @param mixed $id Order ID. * @return object|WP_Error */ private static function order($id) { if (!function_exists('edd_get_order')) { return Helper::error('onemcp-pro_unavailable', __('This Easy Digital Downloads version does not expose edd_get_order.', 'onemcp-pro')); } $order = edd_get_order(Helper::absint($id)); if (!is_object($order) || empty($order->id)) { return Helper::error('onemcp-pro_not_found', __('Order was not found.', 'onemcp-pro')); } return $order; } /** * Allowed order statuses. * * @param mixed $value Requested status. * @return string|WP_Error */ private static function order_status($value) { $status = sanitize_key(is_scalar($value) ? (string) $value : ''); $allowed = array('complete', 'pending', 'processing', 'refunded', 'partially_refunded', 'failed', 'abandoned', 'revoked', 'on_hold'); if (!in_array($status, $allowed, true)) { return Helper::error('onemcp-pro_invalid', __('That order status is not available.', 'onemcp-pro')); } return $status; } /** * License and subscription abilities, only when those extensions are loaded. * * @return array> */ private static function extension_abilities() { $abilities = array(); if (function_exists('edd_software_licensing')) { $abilities[] = Helper::ability( 'list-licenses', __('List licenses', 'onemcp-pro'), __('Lists Software Licensing licenses. Available only while that extension is active.', 'onemcp-pro'), 'onemcp-pro-edd', true, array(__CLASS__, 'list_licenses'), array( 'search' => Helper::string_prop(__('Search license keys.', 'onemcp-pro'), 120), 'limit' => Helper::int_prop(__('How many licenses to return. Maximum 50.', 'onemcp-pro')), 'page' => Helper::int_prop(__('Page number, starting at 1.', 'onemcp-pro')), ), array(), array('permission' => array(__CLASS__, 'can_manage_orders')) ); $abilities[] = Helper::ability( 'get-license', __('Get license', 'onemcp-pro'), __('Returns one Software Licensing license by id or key.', 'onemcp-pro'), 'onemcp-pro-edd', true, array(__CLASS__, 'get_license'), array( 'id' => Helper::int_prop(__('License ID.', 'onemcp-pro')), 'key' => Helper::string_prop(__('License key. Used when id is omitted.', 'onemcp-pro'), 120), ), array(), array('permission' => array(__CLASS__, 'can_manage_orders')) ); $abilities[] = Helper::ability( 'set-license-status', __('Set license status', 'onemcp-pro'), __('Changes a Software Licensing license to active, inactive, disabled, or expired. Set confirm to UPDATE.', 'onemcp-pro'), 'onemcp-pro-edd', false, array(__CLASS__, 'set_license_status'), array( 'id' => Helper::int_prop(__('License ID.', 'onemcp-pro')), 'key' => Helper::string_prop(__('License key. Used when id is omitted.', 'onemcp-pro'), 120), 'status' => Helper::string_prop(__('active, inactive, disabled, or expired.', 'onemcp-pro'), 20), 'confirm' => Helper::string_prop(__('Must be UPDATE.', 'onemcp-pro'), 20), ), array('status', 'confirm'), array('permission' => array(__CLASS__, 'can_manage_orders'), 'destructive' => true) ); } if (class_exists('EDD_Subscriptions_DB') && class_exists('EDD_Subscription')) { $abilities[] = Helper::ability( 'list-subscriptions', __('List subscriptions', 'onemcp-pro'), __('Lists Recurring Payments subscriptions. Available only while that extension is active.', 'onemcp-pro'), 'onemcp-pro-edd', true, array(__CLASS__, 'list_subscriptions'), array( 'status' => Helper::string_prop(__('pending, active, cancelled, expired, trialling, failing, or completed.', 'onemcp-pro'), 20), 'customer_id' => Helper::int_prop(__('Limit to one customer ID.', 'onemcp-pro')), 'product_id' => Helper::int_prop(__('Limit to one download ID.', 'onemcp-pro')), 'search' => Helper::string_prop(__('Search subscriptions.', 'onemcp-pro'), 120), 'limit' => Helper::int_prop(__('How many subscriptions to return. Maximum 50.', 'onemcp-pro')), 'page' => Helper::int_prop(__('Page number, starting at 1.', 'onemcp-pro')), ), array(), array('permission' => array(__CLASS__, 'can_manage_orders')) ); $abilities[] = Helper::ability( 'get-subscription', __('Get subscription', 'onemcp-pro'), __('Returns one Recurring Payments subscription.', 'onemcp-pro'), 'onemcp-pro-edd', true, array(__CLASS__, 'get_subscription'), array('id' => Helper::int_prop(__('Subscription ID.', 'onemcp-pro'))), array('id'), array('permission' => array(__CLASS__, 'can_manage_orders')) ); $abilities[] = Helper::ability( 'set-subscription-status', __('Set subscription status', 'onemcp-pro'), __('Changes a subscription status in Easy Digital Downloads. Notifying the gateway is only used when cancelling. Set confirm to UPDATE.', 'onemcp-pro'), 'onemcp-pro-edd', false, array(__CLASS__, 'set_subscription_status'), array( 'id' => Helper::int_prop(__('Subscription ID.', 'onemcp-pro')), 'status' => Helper::string_prop(__('pending, active, cancelled, expired, trialling, failing, or completed.', 'onemcp-pro'), 20), 'notify_gateway' => Helper::bool_prop(__('When true and status is cancelled, also cancel the subscription at the payment gateway.', 'onemcp-pro')), 'confirm' => Helper::string_prop(__('Must be UPDATE.', 'onemcp-pro'), 20), ), array('id', 'status', 'confirm'), array('permission' => array(__CLASS__, 'can_manage_orders'), 'destructive' => true) ); } return $abilities; } /** * Schema for variable price options. * * @return array */ private static function variable_price_prop() { return array( 'type' => 'array', 'description' => __('Variable prices. Replaces the price options. Each item has name and amount. Set default true on one item.', 'onemcp-pro'), 'items' => array( 'type' => 'object', 'additionalProperties' => false, 'properties' => array( 'name' => array('type' => 'string'), 'amount' => array('type' => 'string'), 'default' => array('type' => 'boolean'), ), 'required' => array('name', 'amount'), ), ); } /** * Schema for downloadable files. * * @return array */ private static function download_file_prop() { return array( 'type' => 'array', 'description' => __('Downloadable files. Replaces the whole file list. An empty list removes every file.', 'onemcp-pro'), 'items' => array( 'type' => 'object', 'additionalProperties' => false, 'properties' => array( 'name' => array('type' => 'string'), 'file' => array('type' => 'string'), 'attachment_id' => array('type' => 'integer'), 'price_id' => array( 'type' => 'string', 'description' => __('all, or the variable price index this file belongs to.', 'onemcp-pro'), ), ), 'required' => array('name'), ), ); } /** * Fields shared by creating and updating a discount. * * @return array */ private static function discount_properties() { return array( 'name' => Helper::string_prop(__('Discount name.', 'onemcp-pro'), 200), 'code' => Helper::string_prop(__('Discount code.', 'onemcp-pro'), 50), 'amount' => Helper::string_prop(__('Discount amount.', 'onemcp-pro'), 20), 'amount_type' => Helper::string_prop(__('percent or flat.', 'onemcp-pro'), 20), 'status' => Helper::string_prop(__('active or inactive. Defaults to active on create.', 'onemcp-pro'), 20), 'max_uses' => Helper::int_prop(__('Maximum uses. 0 is unlimited.', 'onemcp-pro')), 'once_per_customer' => Helper::bool_prop(__('Limit the code to one use per customer.', 'onemcp-pro')), 'min_charge_amount' => Helper::string_prop(__('Minimum order amount required.', 'onemcp-pro'), 20), 'product_ids' => Helper::int_list_prop(__('Downloads this discount requires.', 'onemcp-pro')), 'excluded_product_ids' => Helper::int_list_prop(__('Downloads this discount cannot apply to.', 'onemcp-pro')), 'product_condition' => Helper::string_prop(__('all or any. Used when more than one download is required.', 'onemcp-pro'), 10), 'scope' => Helper::string_prop(__('global or not_global. Defaults to not_global when product_ids is set.', 'onemcp-pro'), 20), 'start_date' => Helper::string_prop(__('Start date, YYYY-MM-DD. An empty string clears it.', 'onemcp-pro'), 20), 'end_date' => Helper::string_prop(__('End date, YYYY-MM-DD. An empty string clears it.', 'onemcp-pro'), 20), ); } /** * Store-wide download link expiration, in hours. * * @param array $input Input. * @return array|WP_Error */ static function update_link_expiration($input) { if (!function_exists('edd_update_option') || !function_exists('edd_get_option')) { return Helper::error('onemcp-pro_unavailable', __('This Easy Digital Downloads version cannot update download link expiration.', 'onemcp-pro')); } if (!array_key_exists('hours', $input)) { return Helper::error('onemcp-pro_invalid', __('Provide the number of hours.', 'onemcp-pro')); } $hours = (int) $input['hours']; if ($hours < 0 || $hours > 8760) { return Helper::error('onemcp-pro_invalid', __('Hours must be from 0 to 8760. 0 means links do not expire.', 'onemcp-pro')); } edd_update_option('download_link_expiration', $hours); return array( 'link_expiration_hours' => absint(edd_get_option('download_link_expiration', $hours)), ); } /** * Update an order email or status. * * @param array $input Input. * @return array|WP_Error */ static function update_order($input) { $order = self::order(isset($input['id']) ? $input['id'] : 0); if (is_wp_error($order)) { return $order; } $changed = false; if (array_key_exists('email', $input)) { if (!function_exists('edd_update_order')) { return Helper::error('onemcp-pro_unavailable', __('This Easy Digital Downloads version cannot update an order.', 'onemcp-pro')); } $email = sanitize_email(is_scalar($input['email']) ? (string) $input['email'] : ''); if (!is_email($email)) { return Helper::error('onemcp-pro_invalid', __('Provide a valid email address.', 'onemcp-pro')); } $updated = edd_update_order($order->id, array('email' => $email)); if (is_wp_error($updated)) { return $updated; } $fresh = function_exists('edd_get_order') ? edd_get_order($order->id) : null; if (!is_object($fresh) || strtolower((string) $fresh->email) !== strtolower($email)) { return Helper::error('onemcp-pro_failed', __('The order email was not changed.', 'onemcp-pro')); } $order = $fresh; $changed = true; } if (array_key_exists('status', $input)) { $status = self::order_status($input['status']); if (is_wp_error($status)) { return $status; } if ((string) $order->status !== $status) { if (!function_exists('edd_update_order_status')) { return Helper::error('onemcp-pro_unavailable', __('This Easy Digital Downloads version cannot update order status.', 'onemcp-pro')); } $updated = edd_update_order_status($order->id, $status); if (!$updated) { return Helper::error('onemcp-pro_failed', __('The order status was not changed.', 'onemcp-pro')); } } elseif (!$changed) { return Helper::error('onemcp-pro_failed', __('The order status was not changed.', 'onemcp-pro')); } $changed = true; } if (!$changed) { return Helper::error('onemcp-pro_invalid', __('Provide an email or status.', 'onemcp-pro')); } return self::get_order(array('id' => $order->id)); } /** * Add an order note. * * @param array $input Input. * @return array|WP_Error */ static function add_order_note($input) { if (!function_exists('edd_add_note')) { return Helper::error('onemcp-pro_unavailable', __('This Easy Digital Downloads version cannot add order notes.', 'onemcp-pro')); } $order = self::order(isset($input['id']) ? $input['id'] : 0); if (is_wp_error($order)) { return $order; } $content = Helper::text(isset($input['content']) ? $input['content'] : '', 5000); if ($content === '') { return Helper::error('onemcp-pro_invalid', __('Provide the note text.', 'onemcp-pro')); } $note_id = edd_add_note(array( 'object_id' => $order->id, 'object_type' => 'order', 'content' => $content, 'user_id' => get_current_user_id(), )); if (empty($note_id)) { return Helper::error('onemcp-pro_failed', __('The order note was not added.', 'onemcp-pro')); } return array( 'id' => (int) $note_id, 'order_id' => (int) $order->id, 'content' => $content, ); } /** * Create a discount. * * @param array $input Input. * @return array|WP_Error */ static function create_discount($input) { if (!function_exists('edd_add_discount') || !function_exists('edd_get_discount_by_code')) { return Helper::error('onemcp-pro_unavailable', __('This Easy Digital Downloads version cannot create discounts.', 'onemcp-pro')); } $data = self::discount_payload($input, true); if (is_wp_error($data)) { return $data; } $existing = edd_get_discount_by_code($data['code']); if (is_object($existing) && !empty($existing->id)) { return Helper::error('onemcp-pro_invalid', __('That discount code already exists.', 'onemcp-pro')); } // The adjustment type is discount. percent and flat belong in amount_type. $data['type'] = 'discount'; if (!isset($data['status'])) { $data['status'] = 'active'; } $discount_id = edd_add_discount($data); if (is_wp_error($discount_id) || empty($discount_id)) { return is_wp_error($discount_id) ? $discount_id : Helper::error('onemcp-pro_failed', __('The discount was not created.', 'onemcp-pro')); } $discount = function_exists('edd_get_discount') ? edd_get_discount($discount_id) : null; return is_object($discount) ? self::discount_row($discount) : array('id' => (int) $discount_id); } /** * Update a discount. * * @param array $input Input. * @return array|WP_Error */ static function update_discount($input) { if (!function_exists('edd_update_discount') || !function_exists('edd_get_discount')) { return Helper::error('onemcp-pro_unavailable', __('This Easy Digital Downloads version cannot update discounts.', 'onemcp-pro')); } $id = Helper::absint(isset($input['id']) ? $input['id'] : 0); $discount = edd_get_discount($id); if (!is_object($discount) || empty($discount->id)) { return Helper::error('onemcp-pro_not_found', __('Discount was not found.', 'onemcp-pro')); } $data = self::discount_payload($input, false); if (is_wp_error($data)) { return $data; } if ($data === array()) { return Helper::error('onemcp-pro_invalid', __('Provide a field to update.', 'onemcp-pro')); } if (isset($data['code']) && function_exists('edd_get_discount_by_code')) { $existing = edd_get_discount_by_code($data['code']); if (is_object($existing) && !empty($existing->id) && (int) $existing->id !== (int) $discount->id) { return Helper::error('onemcp-pro_invalid', __('That discount code already exists.', 'onemcp-pro')); } } $columns = array('name', 'code', 'status', 'amount_type', 'amount', 'scope', 'max_uses', 'once_per_customer', 'min_charge_amount', 'start_date', 'end_date'); $changes_columns = false; foreach ($columns as $column) { if (array_key_exists($column, $data)) { $changes_columns = true; break; } } $result = edd_update_discount($discount->id, $data); if (is_wp_error($result)) { return $result; } // A meta-only change can report false when no discount columns were written. if (false === $result && $changes_columns) { return Helper::error('onemcp-pro_failed', __('The discount was not updated.', 'onemcp-pro')); } $fresh = edd_get_discount($discount->id); return is_object($fresh) ? self::discount_row($fresh) : Helper::error('onemcp-pro_failed', __('The discount was not updated.', 'onemcp-pro')); } /** * List customers. * * @param array $input Input. * @return array|WP_Error */ static function list_customers($input) { if (!function_exists('edd_get_customers')) { return Helper::error('onemcp-pro_unavailable', __('This Easy Digital Downloads version does not expose customers.', 'onemcp-pro')); } $limit = Helper::limit(isset($input['limit']) ? $input['limit'] : 20, 20, 50); $page = Helper::page($input); $args = array( 'number' => $limit, 'offset' => ($page - 1) * $limit, 'orderby' => 'id', 'order' => 'DESC', ); if (!empty($input['search'])) { $args['search'] = Helper::text($input['search'], 120); } $items = array(); foreach ((array) edd_get_customers($args) as $customer) { $row = self::customer_row($customer, false); if ($row !== array()) { $items[] = $row; } } return array( 'items' => $items, 'page' => $page, ); } /** * One customer, with recent purchases and download history. * * @param array $input Input. * @return array|WP_Error */ static function get_customer($input) { $customer = self::customer_from_input($input, false); if (is_wp_error($customer)) { return $customer; } return self::customer_row($customer, true); } /** * Earnings, order count, and best sellers. * * @param array $input Input. * @return array|WP_Error */ static function sales_summary($input) { if (!function_exists('edd_get_orders')) { return Helper::error('onemcp-pro_unavailable', __('This Easy Digital Downloads version does not expose orders.', 'onemcp-pro')); } if (array_key_exists('days', $input)) { $days = (int) $input['days']; if ($days < 1 || $days > 365) { return Helper::error('onemcp-pro_invalid', __('Days must be from 1 to 365.', 'onemcp-pro')); } } else { $days = 30; } $start_ts = time() - ($days * DAY_IN_SECONDS); $start = gmdate('Y-m-d H:i:s', $start_ts); $end = gmdate('Y-m-d H:i:s'); $stats = self::stats_totals($start, $end); $sample = self::recent_sales($start_ts); $response = array( 'days' => $days, 'currency' => function_exists('edd_get_currency') ? (string) edd_get_currency() : '', 'earnings' => $stats['earnings'] !== null ? $stats['earnings'] : (string) $sample['earnings'], 'order_count' => $stats['count'] !== null ? $stats['count'] : $sample['count'], 'best_sellers' => $sample['best_sellers'], 'truncated' => $sample['truncated'], ); $notes = array(); if ($stats['earnings'] === null || $stats['count'] === null) { $notes[] = __('Earnings or the order count came from the latest matching orders.', 'onemcp-pro'); } if ($sample['truncated']) { $notes[] = __('Best sellers use at most the latest 100 complete orders in this range.', 'onemcp-pro'); } if ($notes !== array()) { $response['note'] = implode(' ', $notes); } return $response; } /** * File download logs. * * @param array $input Input. * @return array|WP_Error */ static function list_download_logs($input) { if (!function_exists('edd_get_file_download_logs')) { return Helper::error('onemcp-pro_unavailable', __('This Easy Digital Downloads version does not expose download logs.', 'onemcp-pro')); } $limit = Helper::limit(isset($input['limit']) ? $input['limit'] : 20, 20, 50); $page = Helper::page($input); $args = array( 'number' => $limit, 'offset' => ($page - 1) * $limit, 'orderby' => 'id', 'order' => 'DESC', ); foreach (array('product_id', 'customer_id', 'order_id') as $key) { $id = isset($input[ $key ]) ? Helper::absint($input[ $key ]) : 0; if ($id > 0) { $args[ $key ] = $id; } } if (!empty($input['email']) && empty($args['customer_id'])) { $email = sanitize_email(is_scalar($input['email']) ? (string) $input['email'] : ''); if (!is_email($email)) { return Helper::error('onemcp-pro_invalid', __('Provide a valid email address.', 'onemcp-pro')); } if (!function_exists('edd_get_customer_by')) { return Helper::error('onemcp-pro_unavailable', __('This Easy Digital Downloads version cannot look up a customer by email.', 'onemcp-pro')); } $customer = edd_get_customer_by('email', $email); if (!is_object($customer) || empty($customer->id)) { return Helper::error('onemcp-pro_not_found', __('No customer uses that email.', 'onemcp-pro')); } $args['customer_id'] = (int) $customer->id; } return array( 'items' => self::log_rows($args, true), 'page' => $page, ); } /** * Grant or revoke download access. * * A grant without an order inserts a completed zero-total order directly. * It does not call the status transition that emails a receipt. * * @param array $input Input. * @return array|WP_Error */ public static function set_download_access($input) { $action = sanitize_key(isset($input['action']) ? (string) $input['action'] : ''); $confirm = isset($input['confirm']) ? (string) $input['confirm'] : ''; if ($action === 'grant' && $confirm !== 'GRANT') { return Helper::error('onemcp-pro_invalid', __('Set confirm to GRANT.', 'onemcp-pro')); } if ($action === 'revoke' && $confirm !== 'REVOKE') { return Helper::error('onemcp-pro_invalid', __('Set confirm to REVOKE.', 'onemcp-pro')); } if ($action !== 'grant' && $action !== 'revoke') { return Helper::error('onemcp-pro_invalid', __('Action must be grant or revoke.', 'onemcp-pro')); } if (!function_exists('edd_get_order_items') || !function_exists('edd_update_order_item')) { return Helper::error('onemcp-pro_unavailable', __('This Easy Digital Downloads version cannot change download access.', 'onemcp-pro')); } $order_id = isset($input['order_id']) ? Helper::absint($input['order_id']) : 0; $item_id = isset($input['order_item_id']) ? Helper::absint($input['order_item_id']) : 0; $download_id = isset($input['download_id']) ? Helper::absint($input['download_id']) : 0; if ($item_id > 0) { $item = function_exists('edd_get_order_item') ? edd_get_order_item($item_id) : null; if (!is_object($item) || empty($item->id)) { return Helper::error('onemcp-pro_not_found', __('Order item was not found.', 'onemcp-pro')); } if ($order_id > 0 && (int) $item->order_id !== $order_id) { return Helper::error('onemcp-pro_invalid', __('That order item is not on this order.', 'onemcp-pro')); } if ($download_id > 0 && (int) $item->product_id !== $download_id) { return Helper::error('onemcp-pro_invalid', __('That order item is not this download.', 'onemcp-pro')); } $order = self::order($item->order_id); if (is_wp_error($order)) { return $order; } if ($action === 'grant') { $ready = self::grantable_order($order); if (is_wp_error($ready)) { return $ready; } } $saved = self::set_item_status($item, $action === 'grant' ? 'complete' : 'revoked'); if (is_wp_error($saved)) { return $saved; } self::access_note($order->id, $action, isset($item->product_name) ? (string) $item->product_name : ''); return self::access_result($action, (int) $order->id, false); } if ($order_id > 0) { return self::access_on_order($action, $order_id, $download_id, $input); } if ($action === 'revoke') { return Helper::error('onemcp-pro_invalid', __('Revoking access needs an existing order.', 'onemcp-pro')); } if ($download_id <= 0) { return Helper::error('onemcp-pro_invalid', __('Provide a download ID.', 'onemcp-pro')); } $download = self::download_post($download_id); if (is_wp_error($download)) { return $download; } $price_id = self::access_price_id($download_id, $input, true); if (is_wp_error($price_id)) { return $price_id; } $customer = self::customer_from_input($input, true); if (is_wp_error($customer)) { return $customer; } $created = self::add_manual_order($customer, $download, $price_id); if (is_wp_error($created)) { return $created; } return self::access_result('grant', (int) $created, true); } /** * List Software Licensing records stored as edd_license posts. * * @param array $input Input. * @return array|WP_Error */ public static function list_licenses($input) { if (!function_exists('edd_software_licensing')) { return Helper::error('onemcp-pro_unavailable', __('Software Licensing is not active.', 'onemcp-pro')); } if (!post_type_exists('edd_license')) { return Helper::error('onemcp-pro_unavailable', __('This Software Licensing version does not expose edd_license posts. Look up a license by id or key.', 'onemcp-pro')); } $limit = Helper::limit(isset($input['limit']) ? $input['limit'] : 20, 20, 50); $page = Helper::page($input); $args = array( 'post_type' => 'edd_license', 'post_status' => 'any', 'posts_per_page' => $limit, 'paged' => $page, 'orderby' => 'date', 'order' => 'DESC', ); if (!empty($input['search'])) { $args['meta_query'] = array( array( 'key' => '_edd_sl_key', 'value' => Helper::text($input['search'], 120), 'compare' => 'LIKE', ), ); } $query = new WP_Query($args); $items = array(); foreach ($query->posts as $post) { if ($post instanceof WP_Post) { $items[] = self::license_from_post($post); } } return array( 'items' => $items, 'total' => (int) $query->found_posts, 'page' => $page, ); } /** * One license. * * @param array $input Input. * @return array|WP_Error */ static function get_license($input) { $license = self::fetch_license(isset($input['id']) ? $input['id'] : 0, isset($input['key']) ? $input['key'] : ''); if (is_wp_error($license)) { return $license; } return self::license_row($license); } /** * Change a license status and read it back. * * @param array $input Input. * @return array|WP_Error */ static function set_license_status($input) { if (!isset($input['confirm']) || (string) $input['confirm'] !== 'UPDATE') { return Helper::error('onemcp-pro_invalid', __('Set confirm to UPDATE.', 'onemcp-pro')); } $status = self::choice_status(isset($input['status']) ? $input['status'] : '', array('active', 'inactive', 'disabled', 'expired')); if (is_wp_error($status)) { return $status; } $license = self::fetch_license(isset($input['id']) ? $input['id'] : 0, isset($input['key']) ? $input['key'] : ''); if (is_wp_error($license)) { return $license; } $license_id = self::license_id($license); try { $saved_locally = false; if (method_exists($license, 'set_status')) { $license->set_status($status); $saved_locally = true; } if (method_exists($license, 'save')) { if (!$saved_locally) { $license->status = $status; } $license->save(); $saved_locally = true; } if (!$saved_locally && function_exists('edd_software_licensing')) { $licensing = edd_software_licensing(); if (is_object($licensing) && method_exists($licensing, 'set_license_status')) { $licensing->set_license_status($license_id, $status); $saved_locally = true; } } if (!$saved_locally) { return Helper::error('onemcp-pro_unavailable', __('This Software Licensing version cannot change a license status.', 'onemcp-pro')); } } catch (\Throwable $exception) { return Helper::error('onemcp-pro_failed', __('The license status was not changed.', 'onemcp-pro')); } $fresh = self::fetch_license($license_id, ''); if (is_wp_error($fresh)) { return $fresh; } $saved = self::object_value($fresh, array('status')); if ((string) $saved !== $status) { return Helper::error('onemcp-pro_failed', __('The license status was not changed.', 'onemcp-pro')); } return self::license_row($fresh); } /** * List subscriptions. * * @param array $input Input. * @return array|WP_Error */ static function list_subscriptions($input) { if (!class_exists('EDD_Subscriptions_DB')) { return Helper::error('onemcp-pro_unavailable', __('Recurring Payments is not active.', 'onemcp-pro')); } $db = new \EDD_Subscriptions_DB(); if (!is_object($db) || !method_exists($db, 'get_subscriptions')) { return Helper::error('onemcp-pro_unavailable', __('This Recurring Payments version cannot list subscriptions.', 'onemcp-pro')); } $limit = Helper::limit(isset($input['limit']) ? $input['limit'] : 20, 20, 50); $page = Helper::page($input); $args = array( 'number' => $limit, 'offset' => ($page - 1) * $limit, 'orderby' => 'id', 'order' => 'DESC', ); if (!empty($input['status'])) { $status = self::choice_status($input['status'], array('pending', 'active', 'cancelled', 'expired', 'trialling', 'failing', 'completed')); if (is_wp_error($status)) { return $status; } $args['status'] = $status; } $customer_id = isset($input['customer_id']) ? Helper::absint($input['customer_id']) : 0; if ($customer_id > 0) { $args['customer_id'] = $customer_id; } $product_id = isset($input['product_id']) ? Helper::absint($input['product_id']) : 0; if ($product_id > 0) { $args['product_id'] = $product_id; } if (!empty($input['search'])) { $args['search'] = Helper::text($input['search'], 120); } $items = array(); try { $rows = $db->get_subscriptions($args); } catch (\Throwable $exception) { return Helper::error('onemcp-pro_failed', __('Subscriptions could not be listed.', 'onemcp-pro')); } foreach ((array) $rows as $row) { $subscription = self::subscription_row(is_array($row) ? (object) $row : $row); if ($subscription !== array()) { $items[] = $subscription; } } return array( 'items' => $items, 'page' => $page, ); } /** * One subscription. * * @param array $input Input. * @return array|WP_Error */ static function get_subscription($input) { $subscription = self::load_subscription(isset($input['id']) ? $input['id'] : 0); if (is_wp_error($subscription)) { return $subscription; } return self::subscription_row($subscription); } /** * Change a subscription status. * * @param array $input Input. * @return array|WP_Error */ static function set_subscription_status($input) { if (!isset($input['confirm']) || (string) $input['confirm'] !== 'UPDATE') { return Helper::error('onemcp-pro_invalid', __('Set confirm to UPDATE.', 'onemcp-pro')); } $status = self::choice_status(isset($input['status']) ? $input['status'] : '', array('pending', 'active', 'cancelled', 'expired', 'trialling', 'failing', 'completed')); if (is_wp_error($status)) { return $status; } $subscription = self::load_subscription(isset($input['id']) ? $input['id'] : 0); if (is_wp_error($subscription)) { return $subscription; } $notify = !empty($input['notify_gateway']); try { if ($notify) { if ($status !== 'cancelled') { return Helper::error('onemcp-pro_invalid', __('The payment gateway is contacted only when cancelling a subscription.', 'onemcp-pro')); } if (!method_exists($subscription, 'cancel')) { return Helper::error('onemcp-pro_unavailable', __('This Recurring Payments version cannot cancel a subscription at the gateway.', 'onemcp-pro')); } $result = $subscription->cancel(); if (false === $result) { return Helper::error('onemcp-pro_failed', __('The subscription was not cancelled at the payment gateway.', 'onemcp-pro')); } } elseif (method_exists($subscription, 'update')) { $subscription->update(array('status' => $status)); } else { return Helper::error('onemcp-pro_unavailable', __('This Recurring Payments version cannot update a subscription.', 'onemcp-pro')); } } catch (\Throwable $exception) { return Helper::error('onemcp-pro_failed', __('The subscription status was not changed.', 'onemcp-pro')); } $fresh = self::load_subscription($subscription->id); if (is_wp_error($fresh)) { return $fresh; } if ((string) $fresh->status !== $status) { return Helper::error('onemcp-pro_failed', __('The subscription status was not changed.', 'onemcp-pro')); } return self::subscription_row($fresh); } /** * Write price, file, and download-limit changes. * * @param int $post_id Download ID. * @param array $input Input. * @return bool|WP_Error True when a commerce field was written. */ private static function apply_download_commerce($post_id, $input) { $changed = false; if (array_key_exists('prices', $input)) { $saved = self::save_prices($post_id, $input['prices']); if (is_wp_error($saved)) { return $saved; } $changed = true; } elseif (array_key_exists('price', $input)) { $saved = self::save_simple_price($post_id, $input['price']); if (is_wp_error($saved)) { return $saved; } $changed = true; } if (array_key_exists('files', $input)) { $saved = self::save_files($post_id, $input['files']); if (is_wp_error($saved)) { return $saved; } $changed = true; } if (array_key_exists('download_limit', $input)) { $saved = self::save_download_limit($post_id, $input['download_limit']); if (is_wp_error($saved)) { return $saved; } $changed = true; } return $changed; } /** * Replace variable prices. Indexes start at 1, matching Easy Digital Downloads. * * @param int $post_id Download ID. * @param mixed $prices Requested prices. * @return true|WP_Error */ private static function save_prices($post_id, $prices) { if (!is_array($prices) || $prices === array()) { return Helper::error('onemcp-pro_invalid', __('Provide at least one variable price.', 'onemcp-pro')); } $stored = array(); $default_id = 0; $default_amount = ''; $index = 1; foreach ($prices as $price) { if (!is_array($price)) { return Helper::error('onemcp-pro_invalid', __('Each variable price needs a name and amount.', 'onemcp-pro')); } $name = Helper::text(isset($price['name']) ? $price['name'] : '', 100); $amount = onemcp-pro_price(isset($price['amount']) ? (string) $price['amount'] : ''); if ($name === '' || $amount === '' || !is_numeric($amount)) { return Helper::error('onemcp-pro_invalid', __('Each variable price needs a name and amount.', 'onemcp-pro')); } $stored[ $index ] = array( 'index' => $index, 'name' => $name, 'amount' => $amount, ); if ($default_id === 0 && !empty($price['default'])) { $default_id = $index; $default_amount = $amount; } if ($default_amount === '') { $default_amount = $amount; } $index++; } if ($default_id === 0) { $default_id = 1; $default_amount = $stored[1]['amount']; } update_post_meta($post_id, '_variable_pricing', 1); update_post_meta($post_id, 'edd_variable_prices', $stored); update_post_meta($post_id, '_edd_default_price_id', $default_id); update_post_meta($post_id, 'edd_price', $default_amount); return true; } /** * Set the single price. Refuses when variable prices are already stored. * * @param int $post_id Download ID. * @param mixed $price Raw price. * @return true|WP_Error */ private static function save_simple_price($post_id, $price) { $variable = get_post_meta($post_id, 'edd_variable_prices', true); if (is_array($variable) && $variable !== array()) { return Helper::error('onemcp-pro_invalid', __('This download uses variable prices. The simple price was left unchanged.', 'onemcp-pro')); } update_post_meta($post_id, 'edd_price', onemcp-pro_price(is_scalar($price) ? (string) $price : '')); update_post_meta($post_id, '_variable_pricing', 0); return true; } /** * Replace the downloadable file list. * * @param int $post_id Download ID. * @param mixed $files Requested files. * @return true|WP_Error */ private static function save_files($post_id, $files) { if (!is_array($files)) { return Helper::error('onemcp-pro_invalid', __('Files must be a list.', 'onemcp-pro')); } $stored = array(); $index = 0; foreach ($files as $file) { if (!is_array($file)) { return Helper::error('onemcp-pro_invalid', __('Each file needs a name and a file URL or attachment.', 'onemcp-pro')); } $name = Helper::text(isset($file['name']) ? $file['name'] : '', 200); if ($name === '') { return Helper::error('onemcp-pro_invalid', __('Each file needs a name.', 'onemcp-pro')); } $attachment_id = isset($file['attachment_id']) ? absint($file['attachment_id']) : 0; $url = isset($file['file']) ? esc_url_raw((string) $file['file']) : ''; if ($attachment_id > 0) { $attachment = get_post($attachment_id); if (!$attachment instanceof WP_Post || $attachment->post_type !== 'attachment') { return Helper::error('onemcp-pro_not_found', sprintf( /* translators: %d: attachment ID. */ __('File attachment %d was not found.', 'onemcp-pro'), $attachment_id )); } if ($url === '') { $url = (string) wp_get_attachment_url($attachment_id); } } if ($url === '') { return Helper::error('onemcp-pro_invalid', __('Each file needs a file URL or attachment.', 'onemcp-pro')); } $condition = 'all'; if (isset($file['price_id']) && $file['price_id'] !== '' && $file['price_id'] !== null) { $raw = strtolower(trim(is_scalar($file['price_id']) ? (string) $file['price_id'] : '')); if ($raw === 'all') { $condition = 'all'; } elseif (preg_match('/^\d+$/', $raw)) { $condition = $raw; } else { return Helper::error('onemcp-pro_invalid', __('File price_id must be all or a variable price index.', 'onemcp-pro')); } } $stored[ $index ] = array( 'index' => $index, 'attachment_id' => $attachment_id, 'name' => $name, 'file' => $url, 'condition' => $condition, ); $index++; } update_post_meta($post_id, 'edd_download_files', $stored); return true; } /** * Per-customer download limit. Negative values remove the product limit so the store default applies. * * @param int $post_id Download ID. * @param mixed $limit Requested limit. * @return true|WP_Error */ private static function save_download_limit($post_id, $limit) { $limit = (int) $limit; if ($limit < 0) { delete_post_meta($post_id, '_edd_download_limit'); return true; } if ($limit > 5000) { return Helper::error('onemcp-pro_invalid', __('Download limit cannot be higher than 5000.', 'onemcp-pro')); } update_post_meta($post_id, '_edd_download_limit', $limit); return true; } /** * Variable prices for a full download payload. * * @param int $post_id Download ID. * @param array $variable Stored prices. * @return array> */ private static function price_rows($post_id, $variable) { $default = (int) get_post_meta($post_id, '_edd_default_price_id', true); $rows = array(); foreach ($variable as $key => $price) { if (!is_array($price)) { continue; } $index = isset($price['index']) ? (int) $price['index'] : (int) $key; $rows[] = array( 'id' => $index, 'name' => isset($price['name']) ? (string) $price['name'] : '', 'amount' => isset($price['amount']) ? (string) $price['amount'] : '', 'default' => $index === $default, ); } return $rows; } /** * Files for a full download payload. * * @param int $post_id Download ID. * @return array> */ private static function file_rows($post_id) { $files = get_post_meta($post_id, 'edd_download_files', true); $rows = array(); if (!is_array($files)) { return $rows; } foreach ($files as $key => $file) { if (!is_array($file)) { continue; } $rows[] = array( 'index' => isset($file['index']) ? (int) $file['index'] : (int) $key, 'name' => isset($file['name']) ? (string) $file['name'] : '', 'file' => isset($file['file']) ? (string) $file['file'] : '', 'attachment_id' => isset($file['attachment_id']) ? (int) $file['attachment_id'] : 0, 'condition' => isset($file['condition']) ? (string) $file['condition'] : 'all', ); } return $rows; } /** * The effective per-customer limit, when this EDD version can report it. * * @param int $download_id Download ID. * @return int|null */ private static function effective_download_limit($download_id) { if (!function_exists('edd_get_file_download_limit')) { return null; } try { $method = new \ReflectionFunction('edd_get_file_download_limit'); if ($method->getNumberOfRequiredParameters() > 1) { return null; } $result = $method->invoke($download_id); } catch (\Throwable $exception) { return null; } return is_numeric($result) ? (int) $result : null; } /** * Store-wide link expiration in hours, or null when settings are unavailable. * * @return int|null */ private static function link_expiration_hours() { if (!function_exists('edd_get_option')) { return null; } return absint(edd_get_option('download_link_expiration', 24)); } /** * Purchased items. Payment keys are not included. * * @param int $order_id Order ID. * @return array> */ private static function order_item_rows($order_id) { if ($order_id <= 0 || !function_exists('edd_get_order_items')) { return array(); } $items = edd_get_order_items(array( 'order_id' => $order_id, 'number' => 100, 'orderby' => 'id', 'order' => 'ASC', )); $rows = array(); foreach ((array) $items as $item) { if (!is_object($item)) { continue; } $rows[] = array( 'id' => isset($item->id) ? (int) $item->id : 0, 'product_id' => isset($item->product_id) ? (int) $item->product_id : 0, 'product_name' => isset($item->product_name) ? (string) $item->product_name : '', 'price_id' => self::item_price_id($item), 'quantity' => isset($item->quantity) ? (int) $item->quantity : 0, 'amount' => isset($item->amount) ? (string) $item->amount : '', 'total' => isset($item->total) ? (string) $item->total : '', 'status' => isset($item->status) ? (string) $item->status : '', ); } return $rows; } /** * Price id, or null when the item has none. Zero is a real price index. * * @param object $item Order item or log. * @return int|null */ private static function item_price_id($item) { if (!is_object($item) || !isset($item->price_id) || $item->price_id === '' || $item->price_id === null) { return null; } return (int) $item->price_id; } /** * Discount fields safe to show and send back. * * @param object $discount Discount object. * @return array */ private static function discount_row($discount) { if (!is_object($discount) || empty($discount->id)) { return array(); } $id = (int) $discount->id; $amount_type = ''; if (isset($discount->amount_type) && in_array((string) $discount->amount_type, array('percent', 'flat'), true)) { $amount_type = (string) $discount->amount_type; } else { $type = isset($discount->type) ? (string) $discount->type : ''; if (in_array($type, array('percent', 'flat'), true)) { $amount_type = $type; } } $reqs = array(); if (function_exists('edd_get_discount_product_reqs')) { $raw = edd_get_discount_product_reqs($id); if (is_array($raw)) { $reqs = array_values(array_filter(array_map('absint', $raw))); } } $excluded = array(); if (function_exists('edd_get_discount_excluded_products')) { $raw = edd_get_discount_excluded_products($id); if (is_array($raw)) { $excluded = array_values(array_filter(array_map('absint', $raw))); } } $condition = function_exists('edd_get_discount_product_condition') ? (string) edd_get_discount_product_condition($id) : ''; $scope = isset($discount->scope) ? (string) $discount->scope : ''; if ($scope === '' && function_exists('edd_get_discount_scope')) { $scope = (string) edd_get_discount_scope($id); } $raw_type = isset($discount->type) ? (string) $discount->type : ''; return array( 'id' => $id, 'name' => isset($discount->name) ? (string) $discount->name : '', 'code' => isset($discount->code) ? (string) $discount->code : '', 'amount' => isset($discount->amount) ? (string) $discount->amount : '', 'amount_type' => $amount_type, 'type' => $amount_type !== '' ? $amount_type : $raw_type, 'status' => isset($discount->status) ? (string) $discount->status : '', 'max_uses' => isset($discount->max_uses) ? (int) $discount->max_uses : 0, 'use_count' => isset($discount->use_count) ? (int) $discount->use_count : 0, 'once_per_customer' => !empty($discount->once_per_customer), 'min_charge_amount' => isset($discount->min_charge_amount) ? (string) $discount->min_charge_amount : '', 'scope' => $scope, 'start_date' => self::date_text(isset($discount->start_date) ? $discount->start_date : ''), 'end_date' => self::date_text(isset($discount->end_date) ? $discount->end_date : ''), 'product_ids' => $reqs, 'excluded_product_ids' => $excluded, 'product_condition' => $condition, ); } /** * Discount columns and requirement meta for edd_add_discount or edd_update_discount. * * @param array $input Input. * @param bool $creating True while creating. * @return array|WP_Error */ private static function discount_payload($input, $creating) { $data = array(); if (array_key_exists('name', $input)) { $name = Helper::text($input['name'], 200); if ($name === '') { return Helper::error('onemcp-pro_invalid', __('Provide a discount name.', 'onemcp-pro')); } $data['name'] = $name; } if ($creating || array_key_exists('code', $input)) { $code = self::discount_code(isset($input['code']) ? $input['code'] : ''); if (is_wp_error($code)) { return $code; } $data['code'] = $code; if ($creating && !isset($data['name'])) { $data['name'] = $code; } } if ($creating || array_key_exists('amount', $input)) { $amount = self::discount_amount(isset($input['amount']) ? $input['amount'] : ''); if (is_wp_error($amount)) { return $amount; } $data['amount'] = $amount; } if ($creating || array_key_exists('amount_type', $input)) { $raw_type = isset($input['amount_type']) ? $input['amount_type'] : ''; $type = sanitize_key(is_scalar($raw_type) ? (string) $raw_type : ''); if (!in_array($type, array('percent', 'flat'), true)) { return Helper::error('onemcp-pro_invalid', __('Amount type must be percent or flat.', 'onemcp-pro')); } $data['amount_type'] = $type; } if (array_key_exists('status', $input)) { $status = sanitize_key(is_scalar($input['status']) ? (string) $input['status'] : ''); if (!in_array($status, array('active', 'inactive'), true)) { return Helper::error('onemcp-pro_invalid', __('Discount status must be active or inactive.', 'onemcp-pro')); } $data['status'] = $status; } if (array_key_exists('max_uses', $input)) { $max = (int) $input['max_uses']; if ($max < 0) { return Helper::error('onemcp-pro_invalid', __('Maximum uses cannot be negative.', 'onemcp-pro')); } $data['max_uses'] = $max; } if (array_key_exists('once_per_customer', $input)) { $data['once_per_customer'] = !empty($input['once_per_customer']) ? 1 : 0; } if (array_key_exists('min_charge_amount', $input)) { $min = onemcp-pro_price(is_scalar($input['min_charge_amount']) ? (string) $input['min_charge_amount'] : ''); if ($min === '' || !is_numeric($min) || (float) $min < 0) { return Helper::error('onemcp-pro_invalid', __('Minimum charge amount is not a price.', 'onemcp-pro')); } $data['min_charge_amount'] = $min; } $product_ids = null; if (array_key_exists('product_ids', $input)) { if (!is_array($input['product_ids'])) { return Helper::error('onemcp-pro_invalid', __('Product IDs must be a list.', 'onemcp-pro')); } $product_ids = self::download_id_list($input['product_ids']); if (is_wp_error($product_ids)) { return $product_ids; } $data['product_reqs'] = $product_ids; } if (array_key_exists('excluded_product_ids', $input)) { if (!is_array($input['excluded_product_ids'])) { return Helper::error('onemcp-pro_invalid', __('Excluded product IDs must be a list.', 'onemcp-pro')); } $excluded = self::download_id_list($input['excluded_product_ids']); if (is_wp_error($excluded)) { return $excluded; } $data['excluded_products'] = $excluded; } if (array_key_exists('product_condition', $input)) { $condition = sanitize_key(is_scalar($input['product_condition']) ? (string) $input['product_condition'] : ''); if (!in_array($condition, array('all', 'any'), true)) { return Helper::error('onemcp-pro_invalid', __('Product condition must be all or any.', 'onemcp-pro')); } $data['product_condition'] = $condition; } elseif ($creating && !empty($data['product_reqs'])) { $data['product_condition'] = 'all'; } $scope = ''; if (array_key_exists('scope', $input)) { $scope = sanitize_key(is_scalar($input['scope']) ? (string) $input['scope'] : ''); if (!in_array($scope, array('global', 'not_global'), true)) { return Helper::error('onemcp-pro_invalid', __('Scope must be global or not_global.', 'onemcp-pro')); } } if ($scope === 'global' && is_array($product_ids) && $product_ids !== array()) { return Helper::error('onemcp-pro_invalid', __('A global discount cannot require specific downloads.', 'onemcp-pro')); } if ($scope === 'not_global' && is_array($product_ids) && $product_ids === array()) { return Helper::error('onemcp-pro_invalid', __('A product-specific discount needs at least one download.', 'onemcp-pro')); } if ($scope === '' && is_array($product_ids)) { $scope = $product_ids === array() ? 'global' : 'not_global'; } if ($creating && $scope === 'not_global' && $product_ids === null) { return Helper::error('onemcp-pro_invalid', __('A product-specific discount needs at least one download.', 'onemcp-pro')); } if ($scope !== '') { $data['scope'] = $scope; } elseif ($creating) { $data['scope'] = 'global'; } foreach (array('start_date' => false, 'end_date' => true) as $key => $end) { if (!array_key_exists($key, $input)) { continue; } $date = self::discount_date($input[ $key ], $end); if (is_wp_error($date)) { return $date; } if ($date === '' && $creating) { continue; } $data[ $key ] = $date === '' ? null : $date; } return $data; } /** * Uppercase discount code. * * @param mixed $value Raw code. * @return string|WP_Error */ private static function discount_code($value) { $raw = strtoupper(preg_replace('/\s+/', '', Helper::text($value, 50))); if (!is_string($raw)) { $raw = ''; } $code = preg_replace('/[^A-Z0-9_\-]/', '', $raw); if (!is_string($code) || $code === '') { return Helper::error('onemcp-pro_invalid', __('Provide a discount code.', 'onemcp-pro')); } return $code; } /** * Non-negative discount amount. * * @param mixed $value Raw amount. * @return string|WP_Error */ private static function discount_amount($value) { $amount = onemcp-pro_price(is_scalar($value) ? (string) $value : ''); if ($amount === '' || !is_numeric($amount) || (float) $amount < 0) { return Helper::error('onemcp-pro_invalid', __('Provide a discount amount.', 'onemcp-pro')); } return $amount; } /** * Discount boundary timestamp. Empty means clear. * * @param mixed $value Raw date. * @param bool $end True for the end of that day. * @return string|WP_Error */ private static function discount_date($value, $end) { $date = Helper::date($value); if ($date === null) { return Helper::error('onemcp-pro_invalid', __('Use a YYYY-MM-DD date.', 'onemcp-pro')); } if ($date === '') { return ''; } return $end ? $date . ' 23:59:59' : $date . ' 00:00:00'; } /** * Download IDs that exist. * * @param mixed $value Raw list. * @return array|WP_Error */ private static function download_id_list($value) { $ids = Helper::int_list($value); foreach ($ids as $id) { $post = get_post($id); if (!$post instanceof WP_Post || $post->post_type !== 'download') { return Helper::error('onemcp-pro_not_found', sprintf( /* translators: %d: download ID. */ __('Download %d was not found.', 'onemcp-pro'), $id )); } } return $ids; } /** * Y-m-d, or an empty string when the value is blank. * * @param mixed $value Raw date. * @return string */ private static function date_text($value) { $raw = is_scalar($value) ? trim((string) $value) : ''; if ($raw === '' || strpos($raw, '0000-00-00') === 0) { return ''; } $timestamp = strtotime($raw); if (!$timestamp) { return ''; } return wp_date('Y-m-d', $timestamp); } /** * Customer payload. * * @param object $customer Customer. * @param bool $full Include purchases and download history. * @return array */ private static function customer_row($customer, $full) { if (!is_object($customer) || empty($customer->id)) { return array(); } $row = array( 'id' => (int) $customer->id, 'email' => isset($customer->email) ? (string) $customer->email : '', 'name' => isset($customer->name) ? (string) $customer->name : '', 'user_id' => isset($customer->user_id) ? (int) $customer->user_id : 0, 'purchase_count' => isset($customer->purchase_count) ? (int) $customer->purchase_count : 0, 'purchase_value' => isset($customer->purchase_value) ? (string) $customer->purchase_value : '', 'date' => isset($customer->date_created) ? (string) $customer->date_created : '', ); if (!$full) { return $row; } $row['orders'] = self::customer_orders((int) $customer->id); if (function_exists('edd_get_file_download_logs')) { $row['download_logs'] = self::log_rows(array( 'customer_id' => (int) $customer->id, 'number' => 10, 'orderby' => 'id', 'order' => 'DESC', ), false); } else { $row['download_logs'] = array(); $row['download_logs_available'] = false; } return $row; } /** * Recent purchases for one customer. * * @param int $customer_id Customer ID. * @return array> */ private static function customer_orders($customer_id) { if (!function_exists('edd_get_orders')) { return array(); } $orders = edd_get_orders(array( 'customer_id' => $customer_id, 'type' => 'sale', 'number' => 10, 'orderby' => 'date_created', 'order' => 'DESC', )); $rows = array(); foreach ((array) $orders as $order) { $row = self::order_row($order, false); if ($row !== array()) { $rows[] = $row; } } return $rows; } /** * Find a customer, and create one when granting access by email. * * @param array $input Input. * @param bool $create Create a customer for a new email. * @return object|WP_Error */ private static function customer_from_input($input, $create) { $customer_id = isset($input['customer_id']) ? Helper::absint($input['customer_id']) : 0; if ($customer_id > 0) { if (!function_exists('edd_get_customer')) { return Helper::error('onemcp-pro_unavailable', __('This Easy Digital Downloads version does not expose customers.', 'onemcp-pro')); } $customer = edd_get_customer($customer_id); if (!is_object($customer) || empty($customer->id)) { return Helper::error('onemcp-pro_not_found', __('Customer was not found.', 'onemcp-pro')); } return $customer; } $email_raw = isset($input['email']) ? $input['email'] : ''; $email = sanitize_email(is_scalar($email_raw) ? (string) $email_raw : ''); if ($email === '' || !is_email($email)) { return Helper::error('onemcp-pro_invalid', __('Provide a customer id or email.', 'onemcp-pro')); } if (function_exists('edd_get_customer_by')) { $customer = edd_get_customer_by('email', $email); if (is_object($customer) && !empty($customer->id)) { return $customer; } } if (!$create) { return Helper::error('onemcp-pro_not_found', __('Customer was not found.', 'onemcp-pro')); } if (!class_exists('EDD_Customer')) { return Helper::error('onemcp-pro_unavailable', __('This Easy Digital Downloads version cannot create a customer.', 'onemcp-pro')); } $customer = new \EDD_Customer($email); if (!empty($customer->id)) { return $customer; } $user = get_user_by('email', $email); $created = $customer->create(array( 'email' => $email, 'name' => $email, 'user_id' => $user instanceof WP_User ? (int) $user->ID : 0, )); if (empty($customer->id) && is_numeric($created) && function_exists('edd_get_customer')) { $customer = edd_get_customer((int) $created); } if (!is_object($customer) || empty($customer->id)) { return Helper::error('onemcp-pro_failed', __('The customer could not be created.', 'onemcp-pro')); } return $customer; } /** * Store stats for the window, ignoring a stats API that does not match. * * @param string $start Range start, GMT. * @param string $end Range end, GMT. * @return array{earnings:?string,count:?int} */ private static function stats_totals($start, $end) { $totals = array( 'earnings' => null, 'count' => null, ); if (!class_exists('\EDD\Stats')) { return $totals; } try { $stats = new \EDD\Stats(); } catch (\Throwable $exception) { return $totals; } if (method_exists($stats, 'get_order_earnings')) { try { $earnings = $stats->get_order_earnings(array( 'function' => 'SUM', 'output' => 'raw', 'relative' => false, 'revenue_type' => 'net', 'range' => 'other', 'start' => $start, 'end' => $end, )); if (is_numeric($earnings)) { $totals['earnings'] = (string) $earnings; } } catch (\Throwable $exception) { $totals['earnings'] = null; } } if (method_exists($stats, 'get_order_count')) { try { $count = $stats->get_order_count(array( 'function' => 'COUNT', 'output' => 'raw', 'relative' => false, 'range' => 'other', 'start' => $start, 'end' => $end, )); if (is_numeric($count)) { $totals['count'] = (int) $count; } } catch (\Throwable $exception) { $totals['count'] = null; } } return $totals; } /** * Latest complete orders, filtered to the requested window in PHP. * * @param int $start_ts Window start as a timestamp. * @return array{earnings:float,count:int,best_sellers:array>,truncated:bool} */ private static function recent_sales($start_ts) { $orders = edd_get_orders(array( 'number' => 100, 'status__in' => array('complete', 'partially_refunded'), 'type' => 'sale', 'orderby' => 'date_created', 'order' => 'DESC', )); $matched = array(); $fetched = 0; $saw_older = false; foreach ((array) $orders as $order) { if (!is_object($order)) { continue; } $fetched++; $status = isset($order->status) ? (string) $order->status : ''; if (!in_array($status, array('complete', 'partially_refunded'), true)) { continue; } $created = isset($order->date_created) ? strtotime((string) $order->date_created) : false; if ($created && $created < $start_ts) { $saw_older = true; continue; } $matched[] = $order; } $earnings = 0.0; foreach ($matched as $order) { $earnings += isset($order->total) ? (float) $order->total : 0.0; } return array( 'earnings' => $earnings, 'count' => count($matched), 'best_sellers' => self::best_sellers($matched), 'truncated' => $fetched >= 100 && !$saw_older, ); } /** * Top downloads from the same order sample used for the truncated flag. * * @param array $orders Orders. * @return array> */ private static function best_sellers($orders) { if (!function_exists('edd_get_order_items')) { return array(); } $deliverable = self::deliverable_statuses(); $sellers = array(); foreach ($orders as $order) { $items = edd_get_order_items(array( 'order_id' => (int) $order->id, 'number' => 100, )); foreach ((array) $items as $item) { if (!is_object($item)) { continue; } $product_id = isset($item->product_id) ? (int) $item->product_id : 0; $status = isset($item->status) ? (string) $item->status : ''; if ($product_id <= 0 || !in_array($status, $deliverable, true)) { continue; } if (!isset($sellers[ $product_id ])) { $sellers[ $product_id ] = array( 'product_id' => $product_id, 'name' => isset($item->product_name) ? (string) $item->product_name : '', 'quantity' => 0, 'total' => 0.0, ); } $sellers[ $product_id ]['quantity'] += isset($item->quantity) ? (int) $item->quantity : 0; $sellers[ $product_id ]['total'] += isset($item->total) ? (float) $item->total : 0.0; } } $rows = array_values($sellers); usort($rows, function ($left, $right) { $quantity = $right['quantity'] <=> $left['quantity']; if ($quantity !== 0) { return $quantity; } return $right['total'] <=> $left['total']; }); $rows = array_slice($rows, 0, 10); foreach ($rows as $index => $row) { $rows[ $index ]['total'] = (string) $row['total']; } return $rows; } /** * Download log rows. IP is included only for the log ability. * * @param array $args Query args. * @param bool $include_ip Include IP and user agent. * @return array> */ private static function log_rows($args, $include_ip) { if (!function_exists('edd_get_file_download_logs')) { return array(); } $items = array(); foreach ((array) edd_get_file_download_logs($args) as $log) { if (!is_object($log)) { continue; } $product_id = isset($log->product_id) ? (int) $log->product_id : 0; $file_id = isset($log->file_id) ? (int) $log->file_id : 0; $file_name = ''; if ($product_id > 0) { $files = get_post_meta($product_id, 'edd_download_files', true); if (is_array($files) && isset($files[ $file_id ]['name'])) { $file_name = (string) $files[ $file_id ]['name']; } } $row = array( 'id' => isset($log->id) ? (int) $log->id : 0, 'product_id' => $product_id, 'product_name' => $product_id > 0 ? html_entity_decode(get_the_title($product_id), ENT_QUOTES, 'UTF-8') : '', 'file_id' => $file_id, 'file_name' => $file_name, 'order_id' => isset($log->order_id) ? (int) $log->order_id : 0, 'price_id' => self::item_price_id($log), 'customer_id' => isset($log->customer_id) ? (int) $log->customer_id : 0, 'date' => isset($log->date_created) ? (string) $log->date_created : '', ); if ($include_ip) { $agent = isset($log->user_agent) ? (string) $log->user_agent : ''; $row['ip'] = isset($log->ip) ? (string) $log->ip : ''; $row['user_agent'] = Helper::trim_text($agent, 180); } $items[] = $row; } return $items; } /** * Grant or revoke one download on an existing order. * * @param string $action grant or revoke. * @param int $order_id Order ID. * @param int $download_id Download ID. * @param array $input Input. * @return array|WP_Error */ private static function access_on_order($action, $order_id, $download_id, $input) { $order = self::order($order_id); if (is_wp_error($order)) { return $order; } if ($download_id <= 0) { return Helper::error('onemcp-pro_invalid', __('Provide a download ID.', 'onemcp-pro')); } $download = self::download_post($download_id); if (is_wp_error($download)) { return $download; } $price_id = self::access_price_id($download_id, $input, $action === 'grant'); if (is_wp_error($price_id)) { return $price_id; } $items = self::matching_items($order_id, $download_id, $price_id); if ($action === 'revoke') { $deliverable = self::deliverable_statuses(); $targets = array(); foreach ($items as $item) { if (in_array((string) $item->status, $deliverable, true)) { $targets[] = $item; } } if ($targets === array()) { return Helper::error('onemcp-pro_not_found', __('No active download access was found on that order.', 'onemcp-pro')); } foreach ($targets as $item) { $saved = self::set_item_status($item, 'revoked'); if (is_wp_error($saved)) { return $saved; } } } else { $ready = self::grantable_order($order); if (is_wp_error($ready)) { return $ready; } if ($items === array()) { $added = self::add_access_item($order_id, $download, $price_id); if (is_wp_error($added)) { return $added; } } else { foreach ($items as $item) { $saved = self::set_item_status($item, 'complete'); if (is_wp_error($saved)) { return $saved; } } } } self::access_note($order_id, $action, html_entity_decode(get_the_title($download), ENT_QUOTES, 'UTF-8')); return self::access_result($action, $order_id, false); } /** * Access result without the order payment key. * * @param string $action grant or revoke. * @param int $order_id Order ID. * @param bool $created Whether this call created the order. * @return array */ private static function access_result($action, $order_id, $created) { return array( 'action' => $action, 'order_id' => $order_id, 'created_order' => $created, 'items' => self::order_item_rows($order_id), ); } /** * A download post, without the edit-post capability used by product edits. * * @param int $id Download ID. * @return WP_Post|WP_Error */ private static function download_post($id) { $post = get_post(Helper::absint($id)); if (!$post instanceof WP_Post || $post->post_type !== 'download') { return Helper::error('onemcp-pro_not_found', __('Download was not found.', 'onemcp-pro')); } return $post; } /** * Variable price index. Null means the caller did not choose one. * * @param int $download_id Download ID. * @param array $input Input. * @param bool $require_variable Require a price when the download has variable prices. * @return int|null|WP_Error */ private static function access_price_id($download_id, $input, $require_variable) { $variable = get_post_meta($download_id, 'edd_variable_prices', true); $has_variable = is_array($variable) && $variable !== array(); if (!$has_variable) { return null; } if (!array_key_exists('price_id', $input)) { if ($require_variable) { return Helper::error('onemcp-pro_invalid', __('This download has variable prices. Provide price_id.', 'onemcp-pro')); } return null; } $price_id = (int) $input['price_id']; foreach ($variable as $key => $price) { $index = is_array($price) && isset($price['index']) ? (int) $price['index'] : (int) $key; if ($index === $price_id) { return $price_id; } } return Helper::error('onemcp-pro_invalid', __('That price option was not found.', 'onemcp-pro')); } /** * Orders that can receive download access. * * @param object $order Order. * @return true|WP_Error */ private static function grantable_order($order) { $status = isset($order->status) ? (string) $order->status : ''; if (!in_array($status, array('complete', 'partially_refunded'), true)) { return Helper::error('onemcp-pro_invalid', __('Access can be granted on a complete order. Omit the order to record a manual zero-total order instead.', 'onemcp-pro')); } return true; } /** * Order items for one download, optionally one price. * * @param int $order_id Order ID. * @param int $download_id Download ID. * @param int|null $price_id Price index, or null for every price. * @return array */ private static function matching_items($order_id, $download_id, $price_id) { $items = edd_get_order_items(array( 'order_id' => $order_id, 'product_id' => $download_id, 'number' => 100, 'orderby' => 'id', 'order' => 'ASC', )); $matched = array(); foreach ((array) $items as $item) { if (!is_object($item) || empty($item->id)) { continue; } if ($price_id === null || self::item_price_id($item) === $price_id) { $matched[] = $item; } } return $matched; } /** * Statuses that still allow the customer to download. * * @return array */ private static function deliverable_statuses() { if (function_exists('edd_get_deliverable_order_item_statuses')) { $statuses = edd_get_deliverable_order_item_statuses(); if (is_array($statuses) && $statuses !== array()) { return array_map('strval', $statuses); } } return array('complete', 'partially_refunded'); } /** * Set an order item status and read it back. * * @param object $item Order item. * @param string $status Target status. * @return true|WP_Error */ private static function set_item_status($item, $status) { if (!function_exists('edd_update_order_item')) { return Helper::error('onemcp-pro_unavailable', __('This Easy Digital Downloads version cannot update an order item.', 'onemcp-pro')); } $item_id = isset($item->id) ? (int) $item->id : 0; if ($item_id <= 0) { return Helper::error('onemcp-pro_not_found', __('Order item was not found.', 'onemcp-pro')); } if (isset($item->status) && (string) $item->status === $status) { return true; } $updated = edd_update_order_item($item_id, array('status' => $status)); if (function_exists('edd_get_order_item')) { $fresh = edd_get_order_item($item_id); if (is_object($fresh) && (string) $fresh->status === $status) { return true; } } if (false === $updated) { return Helper::error('onemcp-pro_failed', __('The download access was not changed.', 'onemcp-pro')); } return true; } /** * Insert a completed zero-total order. This does not email a receipt. * * @param object $customer Customer. * @param WP_Post $download Download. * @param int|null $price_id Price index. * @return int|WP_Error Order ID. */ private static function add_manual_order($customer, $download, $price_id) { if (!function_exists('edd_add_order') || !function_exists('edd_add_order_item')) { return Helper::error('onemcp-pro_unavailable', __('This Easy Digital Downloads version cannot create an order.', 'onemcp-pro')); } $email = isset($customer->email) ? sanitize_email((string) $customer->email) : ''; if (!is_email($email)) { return Helper::error('onemcp-pro_invalid', __('That customer has no email address.', 'onemcp-pro')); } if (function_exists('edd_generate_order_payment_key')) { $payment_key = edd_generate_order_payment_key($email); } else { $payment_key = md5(uniqid('onemcp-pro', true)); } $mode = (function_exists('edd_is_test_mode') && edd_is_test_mode()) ? 'test' : 'live'; $order_id = edd_add_order(array( 'status' => 'complete', 'type' => 'sale', 'user_id' => isset($customer->user_id) ? (int) $customer->user_id : 0, 'customer_id' => (int) $customer->id, 'email' => $email, 'gateway' => 'manual', 'mode' => $mode, 'currency' => function_exists('edd_get_currency') ? edd_get_currency() : '', 'payment_key' => $payment_key, 'subtotal' => 0, 'discount' => 0, 'tax' => 0, 'total' => 0, 'date_completed' => gmdate('Y-m-d H:i:s'), )); if (empty($order_id)) { return Helper::error('onemcp-pro_failed', __('The download access was not granted.', 'onemcp-pro')); } $item_id = self::add_access_item((int) $order_id, $download, $price_id); if (is_wp_error($item_id)) { if (function_exists('edd_delete_order')) { edd_delete_order($order_id); } return $item_id; } try { if (method_exists($customer, 'attach_payment')) { $customer->attach_payment($order_id, false); } if (method_exists($customer, 'recalculate_stats')) { $customer->recalculate_stats(); } } catch (\Throwable $exception) { // The order and item exist. A stats failure must not remove the grant. } self::access_note((int) $order_id, 'grant', html_entity_decode(get_the_title($download), ENT_QUOTES, 'UTF-8')); return (int) $order_id; } /** * Add a zero-total download item. * * @param int $order_id Order ID. * @param WP_Post $download Download. * @param int|null $price_id Price index. * @return int|WP_Error Item ID. */ private static function add_access_item($order_id, $download, $price_id) { if (!function_exists('edd_add_order_item')) { return Helper::error('onemcp-pro_unavailable', __('This Easy Digital Downloads version cannot add an order item.', 'onemcp-pro')); } $name = html_entity_decode(get_the_title($download), ENT_QUOTES, 'UTF-8'); if ($price_id !== null && function_exists('edd_get_price_option_name')) { $option = edd_get_price_option_name($download->ID, $price_id); if (is_string($option) && $option !== '') { $name .= ' - ' . $option; } } $item_id = edd_add_order_item(array( 'order_id' => $order_id, 'product_id' => (int) $download->ID, 'product_name' => $name, 'price_id' => $price_id, 'quantity' => 1, 'amount' => 0, 'subtotal' => 0, 'discount' => 0, 'tax' => 0, 'total' => 0, 'status' => 'complete', 'type' => 'download', )); if (empty($item_id)) { return Helper::error('onemcp-pro_failed', __('The download access was not granted.', 'onemcp-pro')); } return (int) $item_id; } /** * Record who changed access. Failure to write the note does not undo the change. * * @param int $order_id Order ID. * @param string $action grant or revoke. * @param string $name Download name. * @return void */ private static function access_note($order_id, $action, $name) { if (!function_exists('edd_add_note')) { return; } $user = wp_get_current_user(); $who = ($user instanceof WP_User && $user->display_name !== '') ? $user->display_name : __('a shop manager', 'onemcp-pro'); if ($action === 'revoke') { $content = sprintf( /* translators: 1: download name, 2: user display name. */ __('Download access for %1$s was revoked by %2$s.', 'onemcp-pro'), $name, $who ); } else { $content = sprintf( /* translators: 1: download name, 2: user display name. */ __('Download access for %1$s was granted by %2$s. No receipt was sent.', 'onemcp-pro'), $name, $who ); } edd_add_note(array( 'object_id' => $order_id, 'object_type' => 'order', 'content' => $content, 'user_id' => get_current_user_id(), )); } /** * License row from a post when the license object cannot be loaded. * * @param WP_Post $post License post. * @return array */ private static function license_from_post($post) { $license = self::fetch_license($post->ID, ''); if (!is_wp_error($license)) { return self::license_row($license); } $expiration = get_post_meta($post->ID, '_edd_sl_expiration', true); $when = is_numeric($expiration) && (int) $expiration > 0 ? gmdate('Y-m-d', (int) $expiration) : self::date_text($expiration); return array( 'id' => (int) $post->ID, 'key' => (string) get_post_meta($post->ID, '_edd_sl_key', true), 'status' => (string) get_post_meta($post->ID, '_edd_sl_status', true), 'download_id' => (int) get_post_meta($post->ID, '_edd_sl_download_id', true), 'order_id' => (int) get_post_meta($post->ID, '_edd_sl_payment_id', true), 'expiration' => $when, 'activation_limit' => (int) get_post_meta($post->ID, '_edd_sl_limit', true), ); } /** * Load one license by id or key. * * @param mixed $id License ID. * @param mixed $key License key. * @return object|WP_Error */ private static function fetch_license($id, $key) { if (!function_exists('edd_software_licensing')) { return Helper::error('onemcp-pro_unavailable', __('Software Licensing is not active.', 'onemcp-pro')); } $licensing = edd_software_licensing(); if (!is_object($licensing) || !method_exists($licensing, 'get_license')) { return Helper::error('onemcp-pro_unavailable', __('This Software Licensing version cannot read a license.', 'onemcp-pro')); } $key = is_scalar($key) ? trim((string) $key) : ''; $id = Helper::absint($id); if ($id <= 0 && $key === '') { return Helper::error('onemcp-pro_invalid', __('Provide a license id or key.', 'onemcp-pro')); } try { if ($key !== '' && $id <= 0) { $license = self::license_by_key($licensing, $key); } else { $license = self::invoke_get_license($licensing, $id, false); } } catch (\Throwable $exception) { return Helper::error('onemcp-pro_failed', __('The license could not be read.', 'onemcp-pro')); } if (!is_object($license) || self::license_id($license) <= 0) { return Helper::error('onemcp-pro_not_found', __('License was not found.', 'onemcp-pro')); } return $license; } /** * Find a license from its key without passing an extra argument to an older method. * * @param object $licensing Licensing service. * @param string $key License key. * @return mixed */ private static function license_by_key($licensing, $key) { if (method_exists($licensing, 'get_license_by_key')) { $found = $licensing->get_license_by_key($key); if (is_object($found) && self::license_id($found) > 0) { return $found; } if (is_numeric($found) && (int) $found > 0) { return self::invoke_get_license($licensing, (int) $found, false); } } return self::invoke_get_license($licensing, $key, true); } /** * Call get_license with the number of arguments that method declares. * * @param object $licensing Licensing service. * @param int|string $value License ID or key. * @param bool $by_key True when $value is a key. * @return mixed */ private static function invoke_get_license($licensing, $value, $by_key) { $method = new \ReflectionMethod($licensing, 'get_license'); if ($by_key && $method->getNumberOfParameters() >= 2) { return $licensing->get_license($value, true); } return $licensing->get_license($value); } /** * License fields. Gateway secrets are not part of this object. * * @param object $license License. * @return array */ private static function license_row($license) { $id = self::license_id($license); $expiration = self::object_value($license, array('expiration', 'expires')); if (is_numeric($expiration) && (int) $expiration > 0) { $expiration = gmdate('Y-m-d', (int) $expiration); } elseif (is_string($expiration) && strtolower($expiration) === 'lifetime') { $expiration = 'lifetime'; } else { $expiration = self::date_text($expiration); } $lifetime = false; if (is_object($license) && method_exists($license, 'is_lifetime')) { try { $lifetime = (bool) $license->is_lifetime(); } catch (\Throwable $exception) { $lifetime = false; } } elseif (is_object($license) && isset($license->is_lifetime)) { $lifetime = (bool) $license->is_lifetime; } $limit = self::object_value($license, array('activation_limit', 'license_limit', 'limit')); $count = self::object_value($license, array('activation_count', 'site_count')); return array( 'id' => $id, 'key' => (string) self::object_value($license, array('license_key', 'key')), 'status' => (string) self::object_value($license, array('status')), 'download_id' => (int) self::object_value($license, array('download_id')), 'order_id' => (int) self::object_value($license, array('order_id', 'payment_id')), 'customer_id' => (int) self::object_value($license, array('customer_id')), 'expiration' => $expiration, 'is_lifetime' => $lifetime || $expiration === 'lifetime', 'activation_limit' => $limit === null ? 0 : (int) $limit, 'activation_count' => $count === null ? 0 : (int) $count, ); } /** * Numeric license ID. * * @param object $license License. * @return int */ private static function license_id($license) { $value = self::object_value($license, array('ID', 'id')); return $value === null ? 0 : (int) $value; } /** * First populated object property. * * @param object $object Object. * @param array $keys Property names. * @return mixed|null */ private static function object_value($object, $keys) { if (!is_object($object)) { return null; } foreach ($keys as $key) { if (isset($object->{$key}) && $object->{$key} !== '' && $object->{$key} !== null) { return $object->{$key}; } } return null; } /** * One allowed status. * * @param mixed $value Requested status. * @param array $allowed Allowed values. * @return string|WP_Error */ private static function choice_status($value, $allowed) { $status = sanitize_key(is_scalar($value) ? (string) $value : ''); if (!in_array($status, $allowed, true)) { return Helper::error('onemcp-pro_invalid', __('That status is not available.', 'onemcp-pro')); } return $status; } /** * Load a subscription. An empty object means the ID was not found. * * @param mixed $id Subscription ID. * @return object|WP_Error */ private static function load_subscription($id) { if (!class_exists('EDD_Subscription')) { return Helper::error('onemcp-pro_unavailable', __('Recurring Payments is not active.', 'onemcp-pro')); } try { $subscription = new \EDD_Subscription(Helper::absint($id)); } catch (\Throwable $exception) { return Helper::error('onemcp-pro_failed', __('The subscription could not be read.', 'onemcp-pro')); } if (!is_object($subscription) || empty($subscription->id)) { return Helper::error('onemcp-pro_not_found', __('Subscription was not found.', 'onemcp-pro')); } return $subscription; } /** * Subscription fields. Profile and gateway transaction secrets are omitted. * * @param mixed $subscription Subscription object. * @return array */ private static function subscription_row($subscription) { if (!is_object($subscription) || empty($subscription->id)) { return array(); } return array( 'id' => (int) $subscription->id, 'customer_id' => isset($subscription->customer_id) ? (int) $subscription->customer_id : 0, 'product_id' => isset($subscription->product_id) ? (int) $subscription->product_id : 0, 'status' => isset($subscription->status) ? (string) $subscription->status : '', 'period' => isset($subscription->period) ? (string) $subscription->period : '', 'initial_amount' => isset($subscription->initial_amount) ? (string) $subscription->initial_amount : '', 'recurring_amount' => isset($subscription->recurring_amount) ? (string) $subscription->recurring_amount : '', 'bill_times' => isset($subscription->bill_times) ? (int) $subscription->bill_times : 0, 'expiration' => isset($subscription->expiration) ? (string) $subscription->expiration : '', 'created' => isset($subscription->created) ? (string) $subscription->created : '', 'parent_payment_id' => isset($subscription->parent_payment_id) ? (int) $subscription->parent_payment_id : 0, 'gateway' => isset($subscription->gateway) ? (string) $subscription->gateway : '', ); } /** * EDD is not loaded. * * @return WP_Error */ private static function missing() { return Helper::error('onemcp-pro_unavailable', __('Easy Digital Downloads is not available.', 'onemcp-pro')); } } /** * Decimal string without requiring WooCommerce. * * @param string $value Raw price. * @return string */ function onemcp_pro_price($value) { if (function_exists('wc_format_decimal')) { return wc_format_decimal($value); } $value = preg_replace('/[^0-9.\-]/', '', (string) $value); return is_string($value) ? $value : ''; }