/** * Handles saving updates from the product editor * * Saves all product related information which includes core product data * and supporting elements such as images, digital downloads, tags, * assigned categories, specs and pricing variations. * * @author Jonathan Davis * @since 1.0 * * @param Product $Product * @return void **/ public function save(ShoppProduct $Product) { check_admin_referer('shopp-save-product'); if (!current_user_can('shopp_products')) { wp_die(__('You do not have sufficient permissions to access this page.')); } ShoppSettings()->saveform(); // Save workflow setting $status = $Product->status; // Set publish date if ('publish' == $_POST['status']) { $publishing = isset($_POST['publish']) ? $_POST['publish'] : array(); $fields = array('month' => '', 'date' => '', 'year' => '', 'hour' => '', 'minute' => '', 'meridiem' => ''); $publishdate = join('', array_merge($fields, $publishing)); if (!empty($publishdate)) { $publish =& $_POST['publish']; if ($publish['meridiem'] == "PM" && $publish['hour'] < 12) { $publish['hour'] += 12; } $publish = mktime($publish['hour'], $publish['minute'], 0, $publish['month'], $publish['date'], $publish['year']); $Product->status = 'future'; unset($_POST['status']); } else { unset($_POST['publish']); // Auto set the publish date if not set (or more accurately, if set to an irrelevant timestamp) if ($Product->publish <= 86400) { $Product->publish = null; } } } else { unset($_POST['publish']); $Product->publish = 0; } // Set a unique product slug if (empty($Product->slug)) { $Product->slug = sanitize_title($_POST['name']); } $Product->slug = wp_unique_post_slug($Product->slug, $Product->id, $Product->status, ShoppProduct::posttype(), 0); $Product->featured = 'off'; if (isset($_POST['content'])) { $_POST['description'] = $_POST['content']; } $Product->updates($_POST, array('meta', 'categories', 'prices', 'tags')); do_action('shopp_pre_product_save'); $Product->save(); // Remove deleted images if (!empty($_POST['deleteImages'])) { $deletes = array(); if (strpos($_POST['deleteImages'], ",") !== false) { $deletes = explode(',', $_POST['deleteImages']); } else { $deletes = array($_POST['deleteImages']); } $Product->delete_images($deletes); } // Update image data if (!empty($_POST['images']) && is_array($_POST['images'])) { $Product->link_images($_POST['images']); $Product->save_imageorder($_POST['images']); if (!empty($_POST['imagedetails'])) { $Product->update_images($_POST['imagedetails']); } } // Update Prices if (!empty($_POST['price']) && is_array($_POST['price'])) { // Delete prices that were marked for removal if (!empty($_POST['deletePrices'])) { $deletes = array(); if (strpos($_POST['deletePrices'], ",")) { $deletes = explode(',', $_POST['deletePrices']); } else { $deletes = array($_POST['deletePrices']); } foreach ($deletes as $option) { $Price = new ShoppPrice($option); $Price->delete(); } } $Product->resum(); // Save prices that there are updates for foreach ($_POST['price'] as $i => $priceline) { if (empty($priceline['id'])) { $Price = new ShoppPrice(); $priceline['product'] = $Product->id; } else { $Price = new ShoppPrice($priceline['id']); } $priceline['sortorder'] = array_search($i, $_POST['sortorder']) + 1; $priceline['shipfee'] = Shopp::floatval($priceline['shipfee']); if (isset($priceline['recurring']['trialprice'])) { $priceline['recurring']['trialprice'] = Shopp::floatval($priceline['recurring']['trialprice']); } if ($Price->stock != $priceline['stocked']) { $priceline['stock'] = (int) $priceline['stocked']; do_action('shopp_stock_product', $priceline['stock'], $Price, $Price->stock, $Price->stocklevel); } else { unset($priceline['stocked']); } $Price->updates($priceline); $Price->save(); // Save 'price' meta records after saving the price record if (isset($priceline['dimensions']) && is_array($priceline['dimensions'])) { $priceline['dimensions'] = array_map(array('Shopp', 'floatval'), $priceline['dimensions']); } $settings = array('donation', 'recurring', 'membership', 'dimensions'); $priceline['settings'] = array(); foreach ($settings as $setting) { if (!isset($priceline[$setting])) { continue; } $priceline['settings'][$setting] = $priceline[$setting]; } if (!empty($priceline['settings'])) { shopp_set_meta($Price->id, 'price', 'settings', $priceline['settings']); } if (!empty($priceline['options'])) { shopp_set_meta($Price->id, 'price', 'options', $priceline['options']); } $Product->sumprice($Price); if (!empty($priceline['download'])) { $Price->attach_download($priceline['download']); } if (!empty($priceline['downloadpath'])) { // Attach file specified by URI/path if (!empty($Price->download->id) || empty($Price->download) && $Price->load_download()) { $File = $Price->download; } else { $File = new ProductDownload(); } $stored = false; $tmpfile = sanitize_path($priceline['downloadpath']); $File->storage = false; $Engine = $File->engine(); // Set engine from storage settings $File->parent = $Price->id; $File->context = "price"; $File->type = "download"; $File->name = !empty($priceline['downloadfile']) ? $priceline['downloadfile'] : basename($tmpfile); $File->filename = $File->name; if ($File->found($tmpfile)) { $File->uri = $tmpfile; $stored = true; } else { $stored = $File->store($tmpfile, 'file'); } if ($stored) { $File->readmeta(); $File->save(); } } // END attach file by path/uri } // END foreach() unset($Price); } // END if (!empty($_POST['price'])) $Product->load_sold($Product->id); // Refresh accurate product sales stats $Product->sumup(); // Update taxonomies after pricing summary is generated // Summary table entry is needed for ProductTaxonomy::recount() to // count properly based on aggregate product inventory, see #2968 foreach (get_object_taxonomies(Product::$posttype) as $taxonomy) { $tags = ''; $taxonomy_obj = get_taxonomy($taxonomy); if (isset($_POST['tax_input']) && isset($_POST['tax_input'][$taxonomy])) { $tags = $_POST['tax_input'][$taxonomy]; if (is_array($tags)) { // array = hierarchical, string = non-hierarchical. $tags = array_filter($tags); } } if (current_user_can($taxonomy_obj->cap->assign_terms)) { wp_set_post_terms($Product->id, $tags, $taxonomy); } } // Ensure taxonomy counts are updated on status changes, see #2968 if ($status != $_POST['status']) { $Post = new StdClass(); $Post->ID = $Product->id; $Post->post_type = ShoppProduct::$posttype; wp_transition_post_status($_POST['status'], $Product->status, $Post); } if (!empty($_POST['meta']['options'])) { $_POST['meta']['options'] = stripslashes_deep($_POST['meta']['options']); } else { $_POST['meta']['options'] = false; } // No variation options at all, delete all variation-pricelines if (!empty($Product->prices) && is_array($Product->prices) && (empty($_POST['meta']['options']['v']) || empty($_POST['meta']['options']['a']))) { foreach ($Product->prices as $priceline) { // Skip if not tied to variation options if ($priceline->optionkey == 0) { continue; } if (empty($_POST['meta']['options']['v']) && $priceline->context == "variation" || empty($_POST['meta']['options']['a']) && $priceline->context == "addon") { $Price = new ShoppPrice($priceline->id); $Price->delete(); } } } // Handle product spec/detail data if (!empty($_POST['details']) || !empty($_POST['deletedSpecs'])) { // Delete specs queued for removal $ids = array(); $deletes = array(); if (!empty($_POST['deletedSpecs'])) { if (strpos($_POST['deleteImages'], ",") !== false) { $deletes = explode(',', $_POST['deleteImages']); } else { $deletes = array($_POST['deletedSpecs']); } $ids = db::escape($_POST['deletedSpecs']); $Spec = new Spec(); db::query("DELETE FROM {$Spec->_table} WHERE id IN ({$ids})"); } if (is_array($_POST['details'])) { foreach ($_POST['details'] as $i => $spec) { if (in_array($spec['id'], $deletes)) { continue; } if (isset($spec['new'])) { $Spec = new Spec(); $spec['id'] = ''; $spec['parent'] = $Product->id; } else { $Spec = new Spec($spec['id']); } $spec['sortorder'] = array_search($i, $_POST['details-sortorder']) + 1; $Spec->updates($spec); $Spec->save(); } } } // Save any meta data if (isset($_POST['meta']) && is_array($_POST['meta'])) { foreach ($_POST['meta'] as $name => $value) { if (isset($Product->meta[$name])) { $Meta = $Product->meta[$name]; if (is_array($Meta)) { $Meta = reset($Product->meta[$name]); } } else { $Meta = new ShoppMetaObject(array('parent' => $Product->id, 'context' => 'product', 'type' => 'meta', 'name' => $name)); } $Meta->parent = $Product->id; $Meta->name = $name; $Meta->value = $value; $Meta->save(); } } $Product->load_data(); // Reload data so everything is fresh for shopp_product_saved do_action_ref_array('shopp_product_saved', array(&$Product)); unset($Product); }
/** * Comprehensive product creation through Product Developer API. * * This function will do everything needed for creating a product except * attach product images and products. That is done in the Asset API. :) * You should be able to build an importer from another system using this function. * * It is also possible to update an existing product (by passing the * existing id as part of the $data array) or else you can alternatively * use shopp_update_product() for that. * * @todo possibly remove the capability of passing in an id to update a product * * @api * @since 1.2 * * @param array $data (required) associative array structure containing a single product definition, see _validate_product_data for how this array is structured/validated. * @return Product the created product object, or boolean false on a failure. **/ function shopp_add_product($data = array()) { if (empty($data)) { shopp_debug(__FUNCTION__ . " failed: Empty data parameter."); return false; } $problems = _validate_product_data($data); if (!empty($problems)) { shopp_debug("Problems detected: " . Shopp::object_r($problems)); return false; } $Product = new ShoppProduct(); // Set Product publish status if (isset($data['publish'])) { $Product->publish = _shopp_product_publish_date($data['publish']); if ($Product->publish > 0) { $Product->status = 'future'; } } // Set Product name if (empty($data['name'])) { shopp_debug(__FUNCTION__ . " failed: Missing product name."); } $Product->name = $data['name']; // Set Product slug if (!empty($data['slug'])) { $Product->slug = $data['slug']; } if (empty($Product->slug)) { $Product->slug = sanitize_title($Product->name); } $Product->slug = wp_unique_post_slug($Product->slug, $Product->id, $Product->status, ShoppProduct::posttype(), 0); $Product->updates($data, array('meta', 'categories', 'prices', 'tags', 'publish')); $Product->save(); ShoppProduct::publishset(array($Product->id), $data['publish']['flag'] ? 'publish' : 'draft'); if (empty($Product->id)) { shopp_debug(__FUNCTION__ . " failed: Failure to create new Product object."); return false; } // Product-wide settings $Product->featured = isset($data['featured']) && true === $data['featured'] ? "on" : "off"; $Product->variants = isset($data['variants']) ? "on" : "off"; $Product->addons = isset($data['addons']) ? "on" : "off"; if (isset($data['packaging'])) { $packaging_set = shopp_product_set_packaging($Product->id, $data['packaging']); if (!$packaging_set) { shopp_debug(__FUNCTION__ . " failed: Failure to set packaging setting."); return false; } } // Save Taxonomies // Categories if (isset($data['categories']) && isset($data['categories']['terms'])) { $cats_set = shopp_product_add_categories($Product->id, $data['categories']['terms']); if (!$cats_set) { shopp_debug(__FUNCTION__ . " failed: Failure to add product categories to product."); return false; } } // Tags if (isset($data['tags']) && isset($data['tags']['terms'])) { $tags_set = shopp_product_add_tags($Product->id, $data['tags']['terms']); if (!$tags_set) { shopp_debug(__FUNCTION__ . " failed: Failure to add product tags to product."); return false; } } // Terms if (isset($data['terms']) && isset($data['terms']['terms']) && isset($data['terms']['taxonomy'])) { $terms_set = shopp_product_add_terms($Product->id, $data['terms']['terms'], $data['terms']['taxonomy']); if (!$terms_set) { shopp_debug(__FUNCTION__ . " failed: Failure to add product taxonomy terms to product."); return false; } } // Create Specs if (isset($data['specs'])) { $specs_set = shopp_product_set_specs($Product->id, $data['specs']); if (!$specs_set) { shopp_debug(__FUNCTION__ . " failed: Failure to add product specs to product."); return false; } } $subjects = array(); $prices = array(); // Create Prices if (isset($data['single'])) { if (!empty($data['single'])) { $subjects['product'] = array($data['single']); } } else { if (isset($data['variants'])) { // Construct and Populate variants if (!isset($data['variants']['menu']) || empty($data['variants']['menu'])) { shopp_debug(__FUNCTION__ . " failed: variants menu is empty."); return false; } $new_variants = shopp_product_set_variant_options($Product->id, $data['variants']['menu'], false); $pricekeys = $prices = array(); foreach ($new_variants as $Price) { $prices[$Price->id] = $pricekeys[$Price->optionkey] = $Price; } if (!$prices) { shopp_debug(__FUNCTION__ . " failed: Unable to set variant options."); return false; } $subjects['variants'] = $data['variants']; } } // Create the "product" Price $Price = new ShoppPrice(); $Price->label = __('Price & Delivery', 'Shopp'); $Price->context = 'product'; $Price->product = $Product->id; if (isset($subjects['variants'])) { $Price->type = 'N/A'; } // disabled $Price->save(); $prices[$Price->id] = $productprice = $Price; // Create Addons if (isset($data['addons'])) { if (!isset($data['addons']['menu']) || empty($data['addons']['menu'])) { shopp_debug(__FUNCTION__ . " failed: addons menu is empty"); return false; } $new_addons = shopp_product_set_addon_options($Product->id, $data['addons']['menu'], false); $addon_prices = array(); foreach ($new_addons as $Addon) { $addon_prices[$Addon->id] = $Addon; } if (!$addon_prices) { shopp_debug(__FUNCTION__ . " failed: Unable to set addon options."); return false; } $prices = $prices + $addon_prices; $subjects['addons'] = $data['addons']; } $contexts = array('addons' => 'addon', 'product' => 'product', 'variants' => 'variant'); foreach ($subjects as $pricetype => $variants) { // apply settings for each priceline foreach ($variants as $key => $variant) { if (!is_numeric($key)) { continue; } $price = null; if ('product' == $pricetype) { $price = $productprice->id; } else { // 'option' => 'array', // array option example: Color=>Blue, Size=>Small if (!isset($variant['option']) || empty($variant['option'])) { shopp_debug(__FUNCTION__ . " failed: variant {$key} missing variant options."); return false; } list($optionkey, $options, $label, $mapping) = $Product->optionmap($variant['option'], $variants['menu'], 'variants' == $pricetype ? 'variant' : 'addon'); if ('variants' == $pricetype && isset($pricekeys[$optionkey])) { $price = $pricekeys[$optionkey]->id; } else { // Find the correct Price foreach ($addon_prices as $index => $Price) { if ($Price->options == $options && $Price->label == $label) { $price = $index; break; } } } } if (null === $price || !isset($prices[$price])) { shopp_debug(__FUNCTION__ . " failed: Variant {$key} not valid for this option set."); return false; } // modify each priceline $prices[$price] = shopp_product_set_variant($prices[$price], $variant, $contexts[$pricetype]); if (!$prices[$price]) { shopp_debug(__FUNCTION__ . " failed: Product variant setup failed."); return false; } // save priceline settings if (isset($prices[$price]->settings)) { shopp_set_meta($prices[$price]->id, 'price', 'settings', $prices[$price]->settings); } // We have everything we need to complete this price line $prices[$price]->save(); } //end variants foreach } // end subjects foreach // Reset rollup figures for prices. $Product->resum(); // Calculates aggregate product stats // foreach ( $prices as $Price ) { // $Product->sumprice($Price); // } // Skeleton summary $Summary = new ProductSummary(); $sum_props = array_keys($Summary->_datatypes); // init default summary items foreach ($sum_props as $prop) { if (!isset($Product->{$prop})) { $Product->{$prop} = NULL; } } // Process pricing stats $records = null; foreach ($prices as $Price) { $Product->pricing($records, $Price); } // Saves generated stats to the product summary $Product->sumup(); return shopp_product($Product->id); }