__( 'Product', 'frontglut' ), 'url' => Dashboard::url( 'products', array( 'a' => 'new' ) ), 'cap' => 'frontglut_manage_products' );
return $q;
} );
add_filter( 'frontglut_widgets', array( $this, 'widgets' ) );
add_filter( 'frontglut_field_render_variations', array( $this, 'render_variations' ), 10, 5 );
add_filter( 'frontglut_field_sanitize_variations', function () { return ''; } );
add_filter( 'frontglut_field_render_products', array( $this, 'render_products_field' ), 10, 5 );
add_filter( 'frontglut_field_sanitize_products', function ( $p, $f, $raw ) { return sanitize_text_field( is_array( $raw ) ? '' : $raw ); }, 10, 3 );
add_filter( 'frontglut_register_field_types', function ( $t ) {
$t['variations'] = 'Variations';
$t['products'] = 'Product picker';
return $t;
} );
foreach ( array( 'save', 'trash', 'restore', 'delete', 'duplicate', 'quick', 'bulk', 'import', 'gen_variations', 'add_variation' ) as $a ) {
Ajax::register( 'product_' . $a, array( $this, 'ajax_' . $a ), array( 'cap' => 'frontglut_manage_products' ) );
}
Ajax::register( 'product_export', array( $this, 'ajax_export' ), array( 'cap' => 'frontglut_manage_products' ) );
add_action( 'wp_footer', array( $this, 'flyout' ) );
add_action( 'admin_bar_menu', array( $this, 'admin_bar' ), 90 );
add_filter( 'show_admin_bar', function ( $show ) {
return Request::route( 'frontglut_embed', 'bool', false ) ? false : $show;
}, 99 );
}
/* ---------------- settings ---------------- */
public function defaults( $d ) {
$d['products'] = array(
'default_status' => 'pending',
'allowed_types' => array( 'simple', 'variable', 'grouped', 'external' ),
'hidden_fields' => array(),
'role_hidden' => '',
'limits' => '',
'gallery_limit' => 10,
'allowed_cats' => array(),
'flyout' => 1,
);
return $d;
}
public function schema( $s ) {
$cats = array();
foreach ( (array) get_terms( array( 'taxonomy' => 'product_cat', 'hide_empty' => false, 'number' => 200 ) ) as $t ) {
if ( ! is_wp_error( $t ) ) {
$cats[ $t->term_id ] = $t->name;
}
}
$s['products'] = array( 'label' => __( 'Products', 'frontglut' ), 'order' => 28, 'fields' => array(
'products.default_status' => array( 'type' => 'select', 'label' => __( 'Status for products created by non-managers', 'frontglut' ), 'options' => array( 'publish' => 'Publish', 'pending' => 'Pending review', 'draft' => 'Draft' ) ),
'products.allowed_types' => array( 'type' => 'checkboxes', 'label' => __( 'Product types users can create', 'frontglut' ), 'options' => array( 'simple' => 'Simple', 'variable' => 'Variable', 'grouped' => 'Grouped', 'external' => 'External / affiliate' ) ),
'products.hidden_fields' => array( 'type' => 'checkboxes', 'label' => __( 'Hide these fields for everyone except shop managers', 'frontglut' ), 'options' => self::field_labels() ),
'products.role_hidden' => array( 'type' => 'textarea', 'label' => __( 'Hide fields per role', 'frontglut' ), 'help' => __( 'One per line: role|field1,field2 e.g. contributor|sku,purchase_note', 'frontglut' ) ),
'products.limits' => array( 'type' => 'textarea', 'label' => __( 'Product limit per role', 'frontglut' ), 'help' => __( 'One per line: role|number e.g. author|25', 'frontglut' ) ),
'products.gallery_limit' => array( 'type' => 'number', 'label' => __( 'Gallery images limit', 'frontglut' ), 'min' => 0 ),
'products.allowed_cats' => array( 'type' => 'multiselect', 'label' => __( 'Restrict categories (empty = all)', 'frontglut' ), 'options' => $cats ),
'products.flyout' => array( 'type' => 'checkbox', 'label' => __( 'Show the "Edit product" flyout button on product pages', 'frontglut' ) ),
) );
return $s;
}
public static function field_labels() {
return array( 'sku' => 'SKU', 'global_unique_id' => 'GTIN/UPC/EAN/ISBN', 'sale_price' => 'Sale price + schedule', 'virtual' => 'Virtual', 'downloadable' => 'Downloadable', 'short_description' => 'Short description', 'manage_stock' => 'Inventory management', 'sold_individually' => 'Sold individually', 'weight' => 'Shipping', 'tax_status' => 'Tax', 'attributes' => 'Attributes', 'upsell_ids' => 'Linked products', 'purchase_note' => 'Purchase note', 'menu_order' => 'Menu order', 'catalog_visibility' => 'Catalog visibility', 'featured' => 'Featured flag', 'gallery' => 'Gallery' );
}
/* ---------------- permissions ---------------- */
public static function is_manager( $uid = 0 ) {
$uid = $uid ? $uid : get_current_user_id();
return user_can( $uid, 'manage_woocommerce' ) || user_can( $uid, 'manage_options' ) || (bool) apply_filters( 'frontglut_products_is_manager', false, $uid );
}
public static function can_edit( $product_id, $uid = 0 ) {
$uid = $uid ? $uid : get_current_user_id();
$p = get_post( $product_id );
if ( ! $p || ! in_array( $p->post_type, array( 'product', 'product_variation' ), true ) || ! Caps::can( 'frontglut_manage_products', $uid ) ) {
return false;
}
$ok = self::is_manager( $uid ) || (int) $p->post_author === (int) self::owner_id( $uid );
return (bool) apply_filters( 'frontglut_can_edit_product', $ok, $p, $uid );
}
private function hidden_fields() {
if ( self::is_manager() ) {
return array();
}
$h = (array) Settings::get( 'products.hidden_fields', array() );
$u = wp_get_current_user();
foreach ( preg_split( '/\r\n|\r|\n/', (string) Settings::get( 'products.role_hidden', '' ) ) as $l ) {
$p = array_map( 'trim', explode( '|', $l, 2 ) );
if ( 2 === count( $p ) && in_array( $p[0], (array) $u->roles, true ) ) {
$h = array_merge( $h, array_map( 'trim', explode( ',', $p[1] ) ) );
}
}
$groups = array( 'sale_price' => array( 'sale_price', 'sale_from', 'sale_to' ), 'manage_stock' => array( 'manage_stock', 'stock_quantity', 'backorders', 'low_stock_amount', 'stock_status' ), 'weight' => array( 'weight', 'length', 'width', 'height', 'shipping_class' ), 'tax_status' => array( 'tax_status', 'tax_class' ), 'upsell_ids' => array( 'upsell_ids', 'cross_sell_ids' ), 'downloadable' => array( 'downloadable', 'downloads', 'download_limit', 'download_expiry' ) );
foreach ( $groups as $k => $list ) {
if ( in_array( $k, $h, true ) ) {
$h = array_merge( $h, $list );
}
}
return array_unique( apply_filters( 'frontglut_product_hidden_fields', $h ) );
}
/* ---------------- dashboard ---------------- */
public function menu( $i ) {
$i['products'] = array( 'label' => __( 'Products', 'frontglut' ), 'icon' => 'cart', 'group' => 'store', 'order' => 20, 'cap' => 'frontglut_manage_products', 'render' => array( $this, 'screen' ) );
return $i;
}
public function widgets( $w ) {
$w['low_stock'] = array( 'title' => __( 'Stock alerts', 'frontglut' ), 'order' => 30, 'size' => 1, 'cap' => 'frontglut_manage_products', 'render' => function () {
$low = $this->query_ids( array( 'stock_status' => 'outofstock' ) );
$h = '
' . Ui::stat( __( 'Out of stock', 'frontglut' ), count( $low ) ) . Ui::stat( __( 'Products', 'frontglut' ), (int) $this->count_all() ) . '
';
return $h . '' . Ui::btn( __( 'Manage products', 'frontglut' ), Dashboard::url( 'products' ), 'frontglut-sm' ) . '
';
} );
return $w;
}
private function count_all() {
$args = array( 'post_type' => 'product', 'post_status' => array( 'publish', 'pending', 'draft', 'private' ), 'fields' => 'ids', 'posts_per_page' => 1 );
if ( ! self::is_manager() ) {
$args['author'] = self::owner_id();
}
$q = new \WP_Query( $args );
return $q->found_posts;
}
private function query_ids( array $f ) {
$args = array( 'status' => array( 'publish', 'pending', 'draft', 'private' ), 'limit' => 200, 'return' => 'ids' );
if ( ! self::is_manager() ) {
$args['author'] = self::owner_id();
}
return wc_get_products( array_merge( $args, $f ) );
}
public function screen() {
$a = Request::route( 'a', 'key', 'list' );
$id = Request::route( 'id', 'int', 0 );
if ( 'new' === $a || ( 'edit' === $a && $id ) ) {
return $this->edit_screen( 'new' === $a ? 0 : $id );
}
if ( 'import' === $a ) {
return $this->import_screen();
}
return $this->list_screen();
}
/* ---------------- list ---------------- */
private function list_screen() {
$q = Request::filter( 's', 'text', '' );
$status = Request::filter( 'status', 'key', '' );
$cat = Request::filter( 'cat', 'int', 0 );
$stock = Request::filter( 'stock', 'key', '' );
$type = Request::filter( 'ptype', 'key', '' );
$sort = Request::filter( 'sort', 'key', 'date' );
$pg = Request::page();
$args = array( 'post_type' => 'product', 'post_status' => $status ? $status : array( 'publish', 'pending', 'draft', 'private' ), 'posts_per_page' => 15, 'paged' => $pg, 's' => $q, 'orderby' => 'date', 'order' => 'DESC', 'tax_query' => array(), 'meta_query' => array() );
if ( ! self::is_manager() ) {
$args['author'] = self::owner_id();
}
if ( 'title' === $sort ) {
$args['orderby'] = 'title';
$args['order'] = 'ASC';
} elseif ( 'price' === $sort ) {
$args['orderby'] = 'meta_value_num';
$args['meta_key'] = '_price';
}
if ( $cat ) {
$args['tax_query'][] = array( 'taxonomy' => 'product_cat', 'terms' => array( $cat ) );
}
if ( $type ) {
$args['tax_query'][] = array( 'taxonomy' => 'product_type', 'field' => 'slug', 'terms' => array( $type ) );
}
if ( $stock ) {
$args['meta_query'][] = array( 'key' => '_stock_status', 'value' => $stock );
}
$query = new \WP_Query( $args );
$cats = array();
foreach ( (array) get_terms( array( 'taxonomy' => 'product_cat', 'hide_empty' => false, 'number' => 200 ) ) as $t ) {
if ( ! is_wp_error( $t ) ) {
$cats[ $t->term_id ] = $t->name;
}
}
$filters = Ui::filters( array(
'status' => array( __( 'All statuses', 'frontglut' ), array( 'publish' => 'Published', 'pending' => 'Pending', 'draft' => 'Draft', 'private' => 'Private', 'trash' => 'Trash' ) ),
'cat' => array( __( 'All categories', 'frontglut' ), $cats ),
'ptype' => array( __( 'All types', 'frontglut' ), array( 'simple' => 'Simple', 'variable' => 'Variable', 'grouped' => 'Grouped', 'external' => 'External' ) ),
'stock' => array( __( 'Any stock', 'frontglut' ), array( 'instock' => 'In stock', 'outofstock' => 'Out of stock', 'onbackorder' => 'On backorder' ) ),
'sort' => array( __( 'Newest first', 'frontglut' ), array( 'title' => 'Name A–Z', 'price' => 'Price' ) ),
), array( 's' => $q, 'status' => $status, 'cat' => $cat, 'ptype' => $type, 'stock' => $stock, 'sort' => $sort ) );
$rows = array();
foreach ( $query->posts as $post ) {
$p = wc_get_product( $post->ID );
if ( ! $p ) {
continue;
}
$edit = Dashboard::url( 'products', array( 'a' => 'edit', 'id' => $p->get_id() ) );
$act = '';
if ( 'trash' === $post->post_status ) {
$act = Ui::action_btn( __( 'Restore', 'frontglut' ), 'product_restore', array( 'id' => $p->get_id() ) ) . Ui::action_btn( __( 'Delete forever', 'frontglut' ), 'product_delete', array( 'id' => $p->get_id() ), 'frontglut-danger', true );
} else {
$act = Ui::kses( apply_filters( 'frontglut_product_row_actions', '', $p ) ) . '' . esc_html__( 'Edit', 'frontglut' ) . ' ' . ( 'publish' === $post->post_status ? '' . esc_html__( 'View', 'frontglut' ) . ' ' : '' ) . Ui::action_btn( __( 'Duplicate', 'frontglut' ), 'product_duplicate', array( 'id' => $p->get_id() ) ) . Ui::action_btn( __( 'Trash', 'frontglut' ), 'product_trash', array( 'id' => $p->get_id() ), 'frontglut-danger', true );
}
$quick = '' . esc_html__( 'Quick edit', 'frontglut' ) . ' ' . Ui::form_open( 'product_quick', array( 'after' => 'reload', 'class' => 'frontglut-qe' ) ) . Ui::hidden( 'id', $p->get_id() ) .
'' . esc_html__( 'Name', 'frontglut' ) . '
' .
( $p->is_type( 'variable' ) || $p->is_type( 'grouped' ) ? '' : '' . esc_html__( 'Price', 'frontglut' ) . '
' . esc_html__( 'Sale', 'frontglut' ) . '
' ) .
'' . esc_html__( 'Stock qty', 'frontglut' ) . ' get_manage_stock() ? '' : 'placeholder="not managed"' ) . '>
' .
'' . esc_html__( 'Status', 'frontglut' ) . ' ' . $this->status_options( $post->post_status ) . '
' . Ui::form_close( __( 'Update', 'frontglut' ) ) . ' ';
$img = $p->get_image_id() ? wp_get_attachment_image( $p->get_image_id(), array( 40, 40 ), false, array( 'class' => 'frontglut-thumb' ) ) : ' ';
$rows[] = array(
'_id' => $p->get_id(),
'img' => $img,
'name' => '' . esc_html( $p->get_name() ?: __( '(no name)', 'frontglut' ) ) . ' ' . ( $p->get_sku() ? 'SKU ' . esc_html( $p->get_sku() ) . ' ' : '' ) . $quick,
'price' => $p->get_price_html() ?: '—',
'stock' => $p->is_in_stock() ? ( $p->get_manage_stock() ? Ui::badge( (string) wc_stock_amount( $p->get_stock_quantity() ), 'ok' ) : Ui::badge( __( 'In stock', 'frontglut' ), 'ok' ) ) : Ui::badge( __( 'Out of stock', 'frontglut' ), 'bad' ),
'type' => esc_html( ucfirst( $p->get_type() ) ),
'status' => Ui::status_badge( $post->post_status ),
'act' => '' . $act . '
',
);
}
$bulk = '' . esc_html__( 'Publish', 'frontglut' ) . ' ' . esc_html__( 'Move to draft', 'frontglut' ) . ' ' . esc_html__( 'Trash', 'frontglut' ) . ' ' . esc_html__( 'Mark featured', 'frontglut' ) . ' ' . esc_html__( 'Remove featured', 'frontglut' ) . ' ' . esc_html__( 'Set in stock', 'frontglut' ) . ' ' . esc_html__( 'Set out of stock', 'frontglut' ) . ' ' . esc_html__( 'Apply', 'frontglut' ) . '
';
$head = Ui::header( __( 'Products', 'frontglut' ), Ui::btn( __( 'Import', 'frontglut' ), Dashboard::url( 'products', array( 'a' => 'import' ) ) ) . ' ' . esc_html__( 'Export CSV', 'frontglut' ) . ' ' . Ui::btn( __( 'Add product', 'frontglut' ), Dashboard::url( 'products', array( 'a' => 'new' ) ), 'frontglut-primary' ) );
return $head . $filters . ( $rows ? $bulk : '' ) . Ui::table( array( 'img' => '', 'name' => __( 'Product', 'frontglut' ), 'price' => __( 'Price', 'frontglut' ), 'stock' => __( 'Stock', 'frontglut' ), 'type' => __( 'Type', 'frontglut' ), 'status' => __( 'Status', 'frontglut' ), 'act' => '' ), $rows, array( 'bulk' => true, 'empty' => __( 'No products found.', 'frontglut' ) ) ) . Ui::pager( $query->found_posts, 15, $pg, array( 's' => $q, 'status' => $status, 'cat' => $cat, 'ptype' => $type, 'stock' => $stock, 'sort' => $sort ) );
}
private function status_options( $cur ) {
$o = '';
$allowed = self::is_manager() ? array( 'publish', 'pending', 'draft', 'private' ) : array( 'draft', 'pending' );
if ( ! self::is_manager() && 'publish' === Settings::get( 'products.default_status' ) ) {
$allowed[] = 'publish';
}
if ( ! in_array( $cur, $allowed, true ) ) {
$allowed[] = $cur;
}
foreach ( array_unique( $allowed ) as $s ) {
$o .= '' . esc_html( ucfirst( $s ) ) . ' ';
}
return $o;
}
/* ---------------- edit form ---------------- */
/** Field definitions grouped by tab. Extend via frontglut_product_fields. */
public function fields( $p = null ) {
$types = array();
foreach ( array( 'simple' => 'Simple product', 'variable' => 'Variable product', 'grouped' => 'Grouped product', 'external' => 'External / affiliate' ) as $k => $l ) {
if ( in_array( $k, (array) Settings::get( 'products.allowed_types', array( 'simple', 'variable', 'grouped', 'external' ) ), true ) || ( $p && $p->get_type() === $k ) ) {
$types[ $k ] = $l;
}
}
$f = array( __CLASS__, 'fd' );
$tc = array( '' => 'Standard' );
foreach ( (array) \WC_Tax::get_tax_class_slugs() as $slug ) {
$tc[ $slug ] = ucwords( str_replace( '-', ' ', $slug ) );
}
$sc = array( '' => __( 'No shipping class', 'frontglut' ) );
foreach ( (array) get_terms( array( 'taxonomy' => 'product_shipping_class', 'hide_empty' => false ) ) as $t ) {
if ( ! is_wp_error( $t ) ) {
$sc[ $t->term_id ] = $t->name;
}
}
$groups = array(
'general' => array( 'label' => __( 'General', 'frontglut' ), 'fields' => array(
self::fd( 'name', 'text', __( 'Product name', 'frontglut' ), array( 'required' => 1 ) ),
self::fd( 'product_type', 'select', __( 'Product type', 'frontglut' ), array( 'options' => $types, 'required' => 1, 'width' => '50' ) ),
self::fd( 'status', 'select', __( 'Status', 'frontglut' ), array( 'options' => array( 'draft' => 'Draft', 'pending' => 'Pending review', 'publish' => 'Published', 'private' => 'Private' ), 'width' => '50' ) ),
self::fd( 'regular_price', 'number', __( 'Regular price', 'frontglut' ), array( 'min' => 0, 'width' => '33', 'class' => 'frontglut-for-simple frontglut-for-external' ) ),
self::fd( 'sale_price', 'number', __( 'Sale price', 'frontglut' ), array( 'min' => 0, 'width' => '33', 'class' => 'frontglut-for-simple frontglut-for-external' ) ),
self::fd( 'sale_from', 'date', __( 'Sale from', 'frontglut' ), array( 'width' => '33', 'class' => 'frontglut-for-simple frontglut-for-external' ) ),
self::fd( 'sale_to', 'date', __( 'Sale to', 'frontglut' ), array( 'width' => '33', 'class' => 'frontglut-for-simple frontglut-for-external' ) ),
self::fd( 'product_url', 'url', __( 'Product URL', 'frontglut' ), array( 'class' => 'frontglut-for-external', 'width' => '66' ) ),
self::fd( 'button_text', 'text', __( 'Button text', 'frontglut' ), array( 'class' => 'frontglut-for-external', 'width' => '33' ) ),
self::fd( 'virtual', 'checkbox', __( 'Virtual (no shipping)', 'frontglut' ), array( 'width' => '50', 'class' => 'frontglut-for-simple' ) ),
self::fd( 'downloadable', 'checkbox', __( 'Downloadable', 'frontglut' ), array( 'width' => '50', 'class' => 'frontglut-for-simple' ) ),
self::fd( 'short_description', 'editor', __( 'Short description', 'frontglut' ), array( 'editor' => 'lite' ) ),
self::fd( 'description', 'editor', __( 'Description', 'frontglut' ), array( 'editor' => 'lite' ) ),
) ),
'inventory' => array( 'label' => __( 'Inventory', 'frontglut' ), 'fields' => array(
self::fd( 'sku', 'text', 'SKU', array( 'width' => '50' ) ),
self::fd( 'global_unique_id', 'text', __( 'GTIN, UPC, EAN or ISBN', 'frontglut' ), array( 'width' => '50' ) ),
self::fd( 'manage_stock', 'checkbox', __( 'Track stock quantity', 'frontglut' ), array( 'class' => 'frontglut-for-simple frontglut-for-variable' ) ),
self::fd( 'stock_quantity', 'number', __( 'Quantity', 'frontglut' ), array( 'width' => '33', 'class' => 'frontglut-if-manage_stock frontglut-for-simple frontglut-for-variable' ) ),
self::fd( 'backorders', 'select', __( 'Allow backorders?', 'frontglut' ), array( 'options' => array( 'no' => 'Do not allow', 'notify' => 'Allow, but notify', 'yes' => 'Allow' ), 'width' => '33', 'class' => 'frontglut-if-manage_stock frontglut-for-simple frontglut-for-variable' ) ),
self::fd( 'low_stock_amount', 'number', __( 'Low stock threshold', 'frontglut' ), array( 'width' => '33', 'class' => 'frontglut-if-manage_stock frontglut-for-simple frontglut-for-variable' ) ),
self::fd( 'stock_status', 'select', __( 'Stock status', 'frontglut' ), array( 'options' => array( 'instock' => 'In stock', 'outofstock' => 'Out of stock', 'onbackorder' => 'On backorder' ), 'class' => 'frontglut-ifnot-manage_stock frontglut-for-simple frontglut-for-external' ) ),
self::fd( 'sold_individually', 'checkbox', __( 'Sold individually (limit 1 per order)', 'frontglut' ), array( 'class' => 'frontglut-for-simple frontglut-for-variable' ) ),
) ),
'shipping' => array( 'label' => __( 'Shipping', 'frontglut' ), 'fields' => array(
self::fd( 'weight', 'number', __( 'Weight', 'frontglut' ) . ' (' . get_option( 'woocommerce_weight_unit' ) . ')', array( 'min' => 0, 'width' => '25' ) ),
self::fd( 'length', 'number', __( 'Length', 'frontglut' ), array( 'min' => 0, 'width' => '25' ) ),
self::fd( 'width', 'number', __( 'Width', 'frontglut' ), array( 'min' => 0, 'width' => '25' ) ),
self::fd( 'height', 'number', __( 'Height', 'frontglut' ), array( 'min' => 0, 'width' => '25' ) ),
self::fd( 'shipping_class', 'select', __( 'Shipping class', 'frontglut' ), array( 'options' => $sc ) ),
), 'class' => 'frontglut-for-simple frontglut-for-variable frontglut-ifnot-virtual' ),
'tax' => array( 'label' => __( 'Tax', 'frontglut' ), 'fields' => array(
self::fd( 'tax_status', 'select', __( 'Tax status', 'frontglut' ), array( 'options' => array( 'taxable' => 'Taxable', 'shipping' => 'Shipping only', 'none' => 'None' ), 'width' => '50' ) ),
self::fd( 'tax_class', 'select', __( 'Tax class', 'frontglut' ), array( 'options' => $tc, 'width' => '50' ) ),
) ),
'downloads' => array( 'label' => __( 'Downloads', 'frontglut' ), 'fields' => array(
self::fd( 'downloads', 'table', __( 'Downloadable files', 'frontglut' ), array( 'columns' => 'File name,File URL', 'help' => __( 'Use full https:// links or files from your uploads folder.', 'frontglut' ) ) ),
self::fd( 'download_limit', 'number', __( 'Download limit', 'frontglut' ), array( 'min' => 0, 'width' => '50', 'help' => __( 'Leave empty for unlimited.', 'frontglut' ) ) ),
self::fd( 'download_expiry', 'number', __( 'Download expiry (days)', 'frontglut' ), array( 'min' => 0, 'width' => '50' ) ),
), 'class' => 'frontglut-for-simple frontglut-if-downloadable' ),
'attributes' => array( 'label' => __( 'Attributes', 'frontglut' ), 'fields' => array(
self::fd( 'attributes', 'table', __( 'Attributes', 'frontglut' ), array( 'columns' => 'Name,Values (separate with |),Visible (1/0),Used for variations (1/0)', 'help' => __( 'Example: Color | Red | Blue | Green. Use a global attribute slug like pa_color to link to a store-wide attribute.', 'frontglut' ) ) ),
), 'class' => 'frontglut-for-simple frontglut-for-variable' ),
'variations' => array( 'label' => __( 'Variations', 'frontglut' ), 'fields' => array(
self::fd( 'variations', 'variations', '' ),
), 'class' => 'frontglut-for-variable' ),
'linked' => array( 'label' => __( 'Linked products', 'frontglut' ), 'fields' => array(
self::fd( 'upsell_ids', 'products', __( 'Upsells', 'frontglut' ), array( 'help' => __( 'Product IDs or SKUs, comma separated.', 'frontglut' ) ) ),
self::fd( 'cross_sell_ids', 'products', __( 'Cross-sells', 'frontglut' ), array( 'help' => __( 'Product IDs or SKUs, comma separated.', 'frontglut' ) ) ),
self::fd( 'grouped_products', 'products', __( 'Grouped products', 'frontglut' ), array( 'class' => 'frontglut-for-grouped', 'help' => __( 'Child product IDs or SKUs, comma separated.', 'frontglut' ) ) ),
) ),
'advanced' => array( 'label' => __( 'Advanced', 'frontglut' ), 'fields' => array(
self::fd( 'purchase_note', 'textarea', __( 'Purchase note', 'frontglut' ) ),
self::fd( 'menu_order', 'number', __( 'Menu order', 'frontglut' ), array( 'width' => '33' ) ),
self::fd( 'catalog_visibility', 'select', __( 'Catalog visibility', 'frontglut' ), array( 'options' => array( 'visible' => 'Shop and search', 'catalog' => 'Shop only', 'search' => 'Search only', 'hidden' => 'Hidden' ), 'width' => '33' ) ),
self::fd( 'featured', 'checkbox', __( 'Featured product', 'frontglut' ), array( 'width' => '33' ) ),
self::fd( 'reviews_allowed', 'checkbox', __( 'Enable reviews', 'frontglut' ) ),
) ),
);
return apply_filters( 'frontglut_product_fields', $groups, $p );
}
public static function fd( $key, $type, $label, $extra = array() ) {
return array_merge( Fields::defaults(), array( 'key' => $key, 'type' => $type, 'label' => $label ), $extra );
}
/** Sidebar taxonomy/image fields. */
private function side_fields() {
$allowed = (array) Settings::get( 'products.allowed_cats', array() );
$f = array();
$f[] = self::fd( 'featured_image', 'image', __( 'Product image', 'frontglut' ) );
$f[] = self::fd( 'gallery', 'gallery', __( 'Gallery', 'frontglut' ) );
$f[] = self::fd( 'cats', 'taxonomy', __( 'Categories', 'frontglut' ), array( 'taxonomy' => 'product_cat', 'ui' => 'checkboxes' ) );
$f[] = self::fd( 'new_cat', 'text', __( 'Add new category', 'frontglut' ), array( 'placeholder' => __( 'Category name', 'frontglut' ) ) );
$f[] = self::fd( 'tags', 'taxonomy', __( 'Tags', 'frontglut' ), array( 'taxonomy' => 'product_tag', 'ui' => 'tags', 'help' => __( 'Separate with commas.', 'frontglut' ) ) );
foreach ( get_object_taxonomies( 'product', 'objects' ) as $t ) {
if ( $t->public && ! in_array( $t->name, array( 'product_cat', 'product_tag', 'product_type', 'product_visibility', 'product_shipping_class' ), true ) && 0 !== strpos( $t->name, 'pa_' ) ) {
$f[] = self::fd( 'tax_' . $t->name, 'taxonomy', $t->labels->name, array( 'taxonomy' => $t->name, 'ui' => $t->hierarchical ? 'checkboxes' : 'tags' ) );
}
}
return $f;
}
private function product_values( $p ) {
$v = array();
if ( ! $p ) {
return apply_filters( 'frontglut_product_values', array( 'product_type' => 'simple', 'status' => 'publish' === Settings::get( 'products.default_status' ) || self::is_manager() ? ( self::is_manager() ? 'publish' : 'publish' ) : Settings::get( 'products.default_status' ), 'tax_status' => 'taxable', 'backorders' => 'no', 'stock_status' => 'instock', 'catalog_visibility' => 'visible', 'reviews_allowed' => 1 ), null );
}
$v = array(
'name' => $p->get_name(), 'product_type' => $p->get_type(), 'status' => $p->get_status(), 'regular_price' => $p->get_regular_price(), 'sale_price' => $p->get_sale_price(),
'sale_from' => $p->get_date_on_sale_from() ? $p->get_date_on_sale_from()->date( 'Y-m-d' ) : '', 'sale_to' => $p->get_date_on_sale_to() ? $p->get_date_on_sale_to()->date( 'Y-m-d' ) : '',
'virtual' => $p->is_virtual() ? 1 : '', 'downloadable' => $p->is_downloadable() ? 1 : '', 'short_description' => $p->get_short_description(), 'description' => $p->get_description(),
'sku' => $p->get_sku(), 'global_unique_id' => method_exists( $p, 'get_global_unique_id' ) ? $p->get_global_unique_id() : '', 'manage_stock' => $p->get_manage_stock() ? 1 : '', 'stock_quantity' => $p->get_stock_quantity(),
'backorders' => $p->get_backorders(), 'low_stock_amount' => $p->get_low_stock_amount(), 'stock_status' => $p->get_stock_status(), 'sold_individually' => $p->get_sold_individually() ? 1 : '',
'weight' => $p->get_weight(), 'length' => $p->get_length(), 'width' => $p->get_width(), 'height' => $p->get_height(), 'shipping_class' => $p->get_shipping_class_id(),
'tax_status' => $p->get_tax_status(), 'tax_class' => $p->get_tax_class(), 'download_limit' => $p->get_download_limit() > 0 ? $p->get_download_limit() : '', 'download_expiry' => $p->get_download_expiry() > 0 ? $p->get_download_expiry() : '',
'purchase_note' => $p->get_purchase_note(), 'menu_order' => $p->get_menu_order(), 'catalog_visibility' => $p->get_catalog_visibility(), 'featured' => $p->get_featured() ? 1 : '', 'reviews_allowed' => $p->get_reviews_allowed() ? 1 : '',
'upsell_ids' => implode( ', ', $p->get_upsell_ids() ), 'cross_sell_ids' => implode( ', ', $p->get_cross_sell_ids() ),
'featured_image' => $p->get_image_id() ? array( $p->get_image_id() ) : array(), 'gallery' => $p->get_gallery_image_ids(), 'cats' => $p->get_category_ids(), 'tags' => wp_list_pluck( (array) get_the_terms( $p->get_id(), 'product_tag' ) ?: array(), 'name' ),
);
if ( $p->is_type( 'external' ) ) {
$v['product_url'] = $p->get_product_url( 'edit' );
$v['button_text'] = $p->get_button_text( 'edit' );
}
if ( $p->is_type( 'grouped' ) ) {
$v['grouped_products'] = implode( ', ', $p->get_children() );
}
$dl = array();
foreach ( $p->get_downloads() as $d ) {
$dl[] = array( $d->get_name(), $d->get_file() );
}
$v['downloads'] = $dl;
$at = array();
foreach ( $p->get_attributes() as $a ) {
$vals = $a->is_taxonomy() ? wp_list_pluck( wc_get_product_terms( $p->get_id(), $a->get_name(), array( 'fields' => 'all' ) ), 'name' ) : $a->get_options();
$at[] = array( $a->get_name(), implode( ' | ', $vals ), $a->get_visible() ? 1 : 0, $a->get_variation() ? 1 : 0 );
}
$v['attributes'] = $at;
foreach ( get_object_taxonomies( 'product', 'objects' ) as $t ) {
if ( $t->public && ! in_array( $t->name, array( 'product_cat', 'product_tag', 'product_type', 'product_visibility', 'product_shipping_class' ), true ) && 0 !== strpos( $t->name, 'pa_' ) ) {
$ts = wp_get_object_terms( $p->get_id(), $t->name );
$v[ 'tax_' . $t->name ] = $t->hierarchical ? wp_list_pluck( is_wp_error( $ts ) ? array() : $ts, 'term_id' ) : wp_list_pluck( is_wp_error( $ts ) ? array() : $ts, 'name' );
}
}
return apply_filters( 'frontglut_product_values', $v, $p );
}
private function edit_screen( $id ) {
$p = null;
if ( $id ) {
if ( ! self::can_edit( $id ) ) {
return Ui::notice( esc_html__( 'You cannot edit this product.', 'frontglut' ), 'warn' );
}
$p = wc_get_product( $id );
if ( ! $p ) {
return Ui::notice( esc_html__( 'Product not found.', 'frontglut' ), 'warn' );
}
} elseif ( ! $this->under_limit( get_current_user_id() ) ) {
return Ui::notice( esc_html__( 'You reached the product limit for your role.', 'frontglut' ), 'warn' );
}
$this->current = $p;
$embed = Request::route( 'frontglut_embed', 'bool', false );
$vals = $this->product_values( $p );
$hidden = $this->hidden_fields();
$groups = $this->fields( $p );
/* translators: %s: value. */
$title = $p ? sprintf( __( 'Edit: %s', 'frontglut' ), $p->get_name() ) : __( 'Add product', 'frontglut' );
$actions = $embed ? '' : Ui::btn( __( '← Products', 'frontglut' ), Dashboard::url( 'products' ) ) . ( $p && 'publish' === $p->get_status() ? ' ' . esc_html__( 'View', 'frontglut' ) . ' ' : '' );
$tabs = '';
$panes = '';
$first = true;
foreach ( $groups as $gid => $g ) {
$fields = array_values( array_filter( $g['fields'], function ( $f ) use ( $hidden ) {
return ! in_array( $f['key'], $hidden, true );
} ) );
if ( ! $fields ) {
continue;
}
foreach ( $fields as &$f ) {
if ( 'status' === $f['key'] ) {
$labels = array( 'draft' => 'Draft', 'pending' => 'Pending review', 'publish' => 'Published', 'private' => 'Private' );
$allowed = self::is_manager() ? array( 'publish', 'pending', 'draft', 'private' ) : array( 'draft', 'pending' );
if ( ! self::is_manager() && 'publish' === Settings::get( 'products.default_status' ) ) {
$allowed[] = 'publish';
}
$cur = isset( $vals['status'] ) ? $vals['status'] : 'draft';
$f['options'] = array_intersect_key( $labels, array_flip( array_merge( $allowed, array( $cur ) ) ) );
if ( ! $p ) {
$vals['status'] = self::is_manager() ? 'publish' : Settings::get( 'products.default_status', 'pending' );
}
}
}
unset( $f );
$cls = isset( $g['class'] ) ? ' ' . $g['class'] : '';
$tabs .= '
' . esc_html( $g['label'] ) . ' ';
$panes .= '
' . Forms::render_fields( $fields, $vals, 'frontglut_f', 0 ) . '
';
$first = false;
}
$tabs .= '
';
$side = Forms::render_fields( array_values( array_filter( $this->side_fields(), function ( $f ) use ( $hidden ) {
return ! in_array( $f['key'], $hidden, true );
} ) ), $vals, 'frontglut_f', 0 );
$h = Ui::header( $title, $actions ) . Ui::form_open( 'product_save', array( 'after' => $embed ? 'embed' : ( $p ? 'reload' : 'none' ), 'class' => 'frontglut-product-form' ) ) . Ui::hidden( 'id', $id ) . ( $embed ? Ui::hidden( 'embed', 1 ) : '' );
$h .= '';
$h .= Ui::form_close( $p ? __( 'Update product', 'frontglut' ) : __( 'Save product', 'frontglut' ) );
return $h;
}
/* ---------------- helpers ---------------- */
/** The user whose catalogue is being managed (add-ons map staff accounts to their employer). */
public static function owner_id( $uid = 0 ) {
$uid = $uid ? $uid : get_current_user_id();
return (int) apply_filters( 'frontglut_products_owner_id', $uid, $uid );
}
/** May this user create another product (role limits, add-on limits)? */
public static function can_create( $uid = 0 ) {
return ( new self() )->under_limit( $uid ? $uid : get_current_user_id() );
}
private function under_limit( $uid ) {
if ( self::is_manager( $uid ) ) {
return true;
}
$ok = apply_filters( 'frontglut_products_can_create', null, self::owner_id( $uid ), $uid );
if ( null !== $ok ) {
return (bool) $ok;
}
$u = get_userdata( $uid );
foreach ( preg_split( '/\r\n|\r|\n/', (string) Settings::get( 'products.limits', '' ) ) as $l ) {
$p = array_map( 'trim', explode( '|', $l, 2 ) );
if ( 2 === count( $p ) && is_numeric( $p[1] ) && in_array( $p[0], (array) $u->roles, true ) ) {
$q = new \WP_Query( array( 'post_type' => 'product', 'author' => $uid, 'post_status' => array( 'publish', 'pending', 'draft', 'private' ), 'fields' => 'ids', 'posts_per_page' => 1 ) );
if ( $q->found_posts >= (int) $p[1] ) {
return false;
}
}
}
return true;
}
private function ids_from_list( $str, $exclude = 0 ) {
$out = array();
foreach ( array_filter( array_map( 'trim', explode( ',', (string) $str ) ) ) as $tok ) {
$id = is_numeric( $tok ) ? (int) $tok : (int) wc_get_product_id_by_sku( $tok );
if ( $id && $id !== (int) $exclude && 'product' === get_post_type( $id ) ) {
$out[] = $id;
}
}
return array_values( array_unique( $out ) );
}
public function render_products_field( $o, $f, $v, $name, $id ) {
$names = array();
foreach ( $this->ids_from_list( is_array( $v ) ? '' : $v ) as $pid ) {
$names[] = '#' . $pid . ' ' . get_the_title( $pid );
}
return ' ' . ( $names ? '' . esc_html( implode( ' · ', $names ) ) . '
' : '' );
}
public function render_variations( $o, $f, $v, $name, $id ) {
$p = $this->current;
if ( ! $p || ! $p->is_type( 'variable' ) ) {
return '' . esc_html__( 'Save the product as "Variable", add attributes marked "Used for variations", then come back here to generate variations.', 'frontglut' ) . '
';
}
$h = '' . Ui::action_btn( __( 'Generate variations from attributes', 'frontglut' ), 'product_gen_variations', array( 'id' => $p->get_id() ), 'frontglut-primary' ) . Ui::action_btn( __( 'Add blank variation', 'frontglut' ), 'product_add_variation', array( 'id' => $p->get_id() ) ) . '
';
$rows = array();
foreach ( $p->get_children() as $vid ) {
$var = wc_get_product( $vid );
if ( ! $var ) {
continue;
}
$n = esc_attr( $name ) . '[' . $vid . ']';
$attr = wc_get_formatted_variation( $var, true, false );
$rows[] = array(
'v' => '#' . (int) $vid . ' ' . esc_html( $attr ? $attr : __( 'Any attributes', 'frontglut' ) ) . ' ',
'sku' => ' ',
'reg' => ' ',
'sale' => ' ',
'stock' => ' ',
'on' => ' get_status(), true, false ) . '>',
'del' => ' ',
);
}
return $h . Ui::table( array( 'v' => __( 'Variation', 'frontglut' ), 'sku' => 'SKU', 'reg' => __( 'Price', 'frontglut' ), 'sale' => __( 'Sale', 'frontglut' ), 'stock' => __( 'Stock', 'frontglut' ), 'on' => __( 'Enabled', 'frontglut' ), 'del' => __( 'Delete', 'frontglut' ) ), $rows, array( 'empty' => __( 'No variations yet. Mark attributes as "used for variations" and generate them.', 'frontglut' ) ) );
}
/* ---------------- save ---------------- */
private function all_field_defs( $p ) {
$out = array();
foreach ( $this->fields( $p ) as $g ) {
foreach ( $g['fields'] as $f ) {
$out[] = $f;
}
}
foreach ( $this->side_fields() as $f ) {
$out[] = $f;
}
$hidden = $this->hidden_fields();
return array_values( array_filter( $out, function ( $f ) use ( $hidden ) {
return ! in_array( $f['key'], $hidden, true ) && 'variations' !== $f['type'];
} ) );
}
public function ajax_save( $req ) {
$id = (int) ( $req['id'] ?? 0 );
$post = isset( $req['frontglut_f'] ) && is_array( $req['frontglut_f'] ) ? $req['frontglut_f'] : array();
$p = null;
if ( $id ) {
if ( ! self::can_edit( $id ) ) {
Ajax::fail( __( 'You cannot edit this product.', 'frontglut' ), 403 );
}
$p = wc_get_product( $id );
if ( ! $p ) {
Ajax::fail( __( 'Product not found.', 'frontglut' ), 404 );
}
} elseif ( ! $this->under_limit( get_current_user_id() ) ) {
Ajax::fail( __( 'You reached the product limit for your role.', 'frontglut' ) );
}
$defs = $this->all_field_defs( $p );
list( $v, $errors ) = Fields::process( $defs, $post );
// Fields absent from the request are left untouched (except types browsers omit when empty).
$ftype = wp_list_pluck( $defs, 'type', 'key' );
$always = array( 'checkboxes', 'multiselect', 'taxonomy', 'gallery', 'image', 'file', 'table' );
foreach ( array_keys( $v ) as $k ) {
if ( ! array_key_exists( $k, $post ) && ! in_array( isset( $ftype[ $k ] ) ? $ftype[ $k ] : '', $always, true ) ) {
unset( $v[ $k ], $errors[ $k ] );
}
}
$classes = array( 'simple' => 'WC_Product_Simple', 'variable' => 'WC_Product_Variable', 'grouped' => 'WC_Product_Grouped', 'external' => 'WC_Product_External' );
$allowed_types = (array) Settings::get( 'products.allowed_types', array_keys( $classes ) );
$type = isset( $v['product_type'] ) ? $v['product_type'] : ( $p ? $p->get_type() : 'simple' );
if ( ! isset( $classes[ $type ] ) || ( ! in_array( $type, $allowed_types, true ) && ( ! $p || $p->get_type() !== $type ) ) ) {
$errors['product_type'] = __( 'This product type is not allowed.', 'frontglut' );
}
if ( isset( $v['sale_price'], $v['regular_price'] ) && '' !== $v['sale_price'] && '' !== $v['regular_price'] && $v['sale_price'] >= $v['regular_price'] ) {
$errors['sale_price'] = __( 'Sale price must be lower than the regular price.', 'frontglut' );
}
if ( ! empty( $v['sku'] ) && ! wc_product_has_unique_sku( $id, $v['sku'] ) ) {
$errors['sku'] = __( 'This SKU is already used.', 'frontglut' );
}
if ( isset( $v['name'] ) && '' === trim( $v['name'] ) ) {
$errors['name'] = __( 'Product name is required.', 'frontglut' );
}
if ( $errors ) {
Ajax::fail( __( 'Please fix the highlighted fields.', 'frontglut' ), 400, array( 'errors' => $errors ) );
}
$divert = apply_filters( 'frontglut_product_pre_save', null, $p, $v, $post, $id );
if ( is_array( $divert ) ) {
return $divert; // an add-on handled the request (e.g. queued for approval).
}
if ( ! $p ) {
$p = new $classes[ $type ]();
} elseif ( $p->get_type() !== $type ) {
wp_set_object_terms( $p->get_id(), $type, 'product_type' );
$p = new $classes[ $type ]( $p->get_id() );
}
$has = function ( $k ) use ( $v ) {
return array_key_exists( $k, $v );
};
$map = array( 'name' => 'set_name', 'description' => 'set_description', 'short_description' => 'set_short_description', 'purchase_note' => 'set_purchase_note', 'catalog_visibility' => 'set_catalog_visibility', 'tax_status' => 'set_tax_status', 'tax_class' => 'set_tax_class', 'weight' => 'set_weight', 'length' => 'set_length', 'width' => 'set_width', 'height' => 'set_height' );
foreach ( $map as $k => $m ) {
if ( $has( $k ) ) {
$p->$m( $v[ $k ] );
}
}
if ( $has( 'menu_order' ) ) {
$p->set_menu_order( (int) $v['menu_order'] );
}
foreach ( array( 'featured' => 'set_featured', 'reviews_allowed' => 'set_reviews_allowed', 'sold_individually' => 'set_sold_individually' ) as $k => $m ) {
if ( $has( $k ) ) {
$p->$m( ! empty( $v[ $k ] ) );
}
}
if ( $has( 'sku' ) ) {
$p->set_sku( $v['sku'] );
}
if ( $has( 'global_unique_id' ) && method_exists( $p, 'set_global_unique_id' ) ) {
$p->set_global_unique_id( $v['global_unique_id'] );
}
if ( in_array( $type, array( 'simple', 'external' ), true ) ) {
if ( $has( 'regular_price' ) ) {
$p->set_regular_price( $v['regular_price'] );
}
if ( $has( 'sale_price' ) ) {
$p->set_sale_price( $v['sale_price'] );
$p->set_date_on_sale_from( ! empty( $v['sale_from'] ) ? $v['sale_from'] : null );
$p->set_date_on_sale_to( ! empty( $v['sale_to'] ) ? $v['sale_to'] : null );
}
}
if ( 'external' === $type ) {
if ( $has( 'product_url' ) ) {
$p->set_product_url( $v['product_url'] );
}
if ( $has( 'button_text' ) ) {
$p->set_button_text( $v['button_text'] );
}
}
if ( 'simple' === $type ) {
if ( $has( 'virtual' ) ) {
$p->set_virtual( ! empty( $v['virtual'] ) );
}
if ( $has( 'downloadable' ) ) {
$p->set_downloadable( ! empty( $v['downloadable'] ) );
$files = array();
if ( ! empty( $v['downloadable'] ) ) {
$up = wp_upload_dir();
foreach ( (array) ( $v['downloads'] ?? array() ) as $row ) {
$url = isset( $row[1] ) ? esc_url_raw( $row[1] ) : '';
$ok = $url && ( 0 === strpos( $url, 'http://' ) || 0 === strpos( $url, 'https://' ) );
if ( $ok ) {
$d = new \WC_Product_Download();
$d->set_id( md5( $url ) );
$d->set_name( ! empty( $row[0] ) ? $row[0] : wp_basename( $url ) );
$d->set_file( $url );
$files[] = $d;
}
}
}
$p->set_downloads( $files );
$p->set_download_limit( $has( 'download_limit' ) && '' !== $v['download_limit'] ? (int) $v['download_limit'] : -1 );
$p->set_download_expiry( $has( 'download_expiry' ) && '' !== $v['download_expiry'] ? (int) $v['download_expiry'] : -1 );
}
}
if ( in_array( $type, array( 'simple', 'variable' ), true ) && $has( 'manage_stock' ) ) {
$p->set_manage_stock( ! empty( $v['manage_stock'] ) );
if ( ! empty( $v['manage_stock'] ) ) {
$p->set_stock_quantity( isset( $v['stock_quantity'] ) && '' !== $v['stock_quantity'] ? wc_stock_amount( $v['stock_quantity'] ) : 0 );
$p->set_backorders( $v['backorders'] ?? 'no' );
$p->set_low_stock_amount( isset( $v['low_stock_amount'] ) && '' !== $v['low_stock_amount'] ? (int) $v['low_stock_amount'] : '' );
} elseif ( $has( 'stock_status' ) && 'variable' !== $type ) {
$p->set_stock_status( $v['stock_status'] );
}
} elseif ( 'external' === $type && $has( 'stock_status' ) ) {
$p->set_stock_status( $v['stock_status'] );
}
if ( $has( 'shipping_class' ) ) {
$p->set_shipping_class_id( (int) $v['shipping_class'] );
}
if ( $has( 'upsell_ids' ) ) {
$p->set_upsell_ids( $this->ids_from_list( $v['upsell_ids'], (int) $p->get_id() ) );
}
if ( $has( 'cross_sell_ids' ) ) {
$p->set_cross_sell_ids( $this->ids_from_list( $v['cross_sell_ids'], (int) $p->get_id() ) );
}
if ( 'grouped' === $type && $has( 'grouped_products' ) ) {
$p->set_children( $this->ids_from_list( $v['grouped_products'], (int) $p->get_id() ) );
}
if ( $has( 'attributes' ) && in_array( $type, array( 'simple', 'variable' ), true ) ) {
$p->set_attributes( $this->build_attributes( (array) $v['attributes'] ) );
}
// Images.
$claim = array();
if ( $has( 'featured_image' ) ) {
$ids = Uploads::filter_owned( (array) $v['featured_image'] );
$p->set_image_id( $ids ? (int) $ids[0] : 0 );
$claim = array_merge( $claim, $ids );
}
if ( $has( 'gallery' ) ) {
$ids = Uploads::filter_owned( (array) $v['gallery'] );
$lim = (int) Settings::get( 'products.gallery_limit', 10 );
if ( $lim > 0 && ! self::is_manager() ) {
$ids = array_slice( $ids, 0, $lim );
}
$p->set_gallery_image_ids( $ids );
$claim = array_merge( $claim, $ids );
}
// Status.
$status = $has( 'status' ) ? $v['status'] : ( $p->get_id() ? $p->get_status() : 'draft' );
if ( ! self::is_manager() ) {
$allowed = array( 'draft', 'pending' );
if ( 'publish' === Settings::get( 'products.default_status' ) ) {
$allowed[] = 'publish';
}
if ( ! in_array( $status, $allowed, true ) && ! ( $p->get_id() && $status === $p->get_status() ) ) {
$status = Settings::get( 'products.default_status', 'pending' );
}
}
$p->set_status( in_array( $status, array( 'publish', 'pending', 'draft', 'private' ), true ) ? $status : 'draft' );
$pid = $p->save();
if ( ! $pid ) {
Ajax::fail( __( 'Could not save the product.', 'frontglut' ) );
}
// Taxonomies.
if ( $has( 'cats' ) || ! empty( $v['new_cat'] ) ) {
$cats = isset( $v['cats'] ) ? array_map( 'intval', (array) $v['cats'] ) : array();
$allowed_cats = array_map( 'intval', (array) apply_filters( 'frontglut_product_allowed_cats', (array) Settings::get( 'products.allowed_cats', array() ), self::owner_id() ) );
if ( $allowed_cats && ! self::is_manager() ) {
$cats = array_values( array_intersect( $cats, $allowed_cats ) );
}
if ( ! empty( $v['new_cat'] ) && current_user_can( 'manage_product_terms' ) ) {
$t = term_exists( $v['new_cat'], 'product_cat' ) ? get_term_by( 'name', $v['new_cat'], 'product_cat' ) : wp_insert_term( $v['new_cat'], 'product_cat' );
$tid = is_array( $t ) ? $t['term_id'] : ( $t instanceof \WP_Term ? $t->term_id : 0 );
if ( $tid ) {
$cats[] = (int) $tid;
}
}
wp_set_object_terms( $pid, array_values( array_unique( $cats ) ), 'product_cat' );
}
if ( $has( 'tags' ) ) {
wp_set_object_terms( $pid, (array) $v['tags'], 'product_tag' );
}
foreach ( $v as $k => $val ) {
if ( 0 === strpos( $k, 'tax_' ) && taxonomy_exists( substr( $k, 4 ) ) && is_object_in_taxonomy( 'product', substr( $k, 4 ) ) ) {
wp_set_object_terms( $pid, is_array( $val ) && $val && is_numeric( $val[0] ) ? array_map( 'intval', $val ) : (array) $val, substr( $k, 4 ) );
}
}
Uploads::claim( $claim, $pid );
if ( 'variable' === $type && ! empty( $post['variations'] ) && is_array( $post['variations'] ) ) {
$this->save_variations( $pid, $post['variations'] );
\WC_Product_Variable::sync( $pid );
}
wc_delete_product_transients( $pid );
if ( ! $id && ! self::is_manager() ) {
/* translators: %s: value. */
\FrontGlut\Core\Notify::send_to_cap( 'manage_woocommerce', sprintf( __( 'New product: %s', 'frontglut' ), $p->get_name() ), '', Dashboard::url( 'products', array( 'a' => 'edit', 'id' => $pid ) ), get_current_user_id() );
}
Audit::log( $id ? 'product_updated' : 'product_created', (string) $pid );
do_action( 'frontglut_product_saved', $pid, $v, ! $id );
$out = array( 'message' => $id ? __( 'Product updated.', 'frontglut' ) : __( 'Product created.', 'frontglut' ), 'id' => $pid );
if ( ! $id && empty( $req['embed'] ) ) {
$out['redirect'] = Dashboard::url( 'products', array( 'a' => 'edit', 'id' => $pid ) );
}
return $out;
}
private function build_attributes( array $rows ) {
$attrs = array();
$pos = 0;
foreach ( $rows as $r ) {
$name = isset( $r[0] ) ? trim( $r[0] ) : '';
if ( '' === $name ) {
continue;
}
$vals = array_values( array_filter( array_map( 'trim', explode( '|', isset( $r[1] ) ? $r[1] : '' ) ), 'strlen' ) );
$a = new \WC_Product_Attribute();
if ( 0 === strpos( $name, 'pa_' ) && taxonomy_exists( $name ) ) {
$a->set_id( wc_attribute_taxonomy_id_by_name( $name ) );
$a->set_name( $name );
$ids = array();
foreach ( $vals as $tv ) {
$t = get_term_by( 'name', $tv, $name ) ?: get_term_by( 'slug', sanitize_title( $tv ), $name );
if ( ! $t && current_user_can( 'manage_product_terms' ) ) {
$ins = wp_insert_term( $tv, $name );
$t = is_wp_error( $ins ) ? null : get_term( $ins['term_id'], $name );
}
if ( $t ) {
$ids[] = (int) $t->term_id;
}
}
$a->set_options( $ids );
} else {
$a->set_id( 0 );
$a->set_name( wc_clean( $name ) );
$a->set_options( array_map( 'wc_clean', $vals ) );
}
$a->set_position( $pos++ );
$a->set_visible( ! isset( $r[2] ) || '' === trim( $r[2] ) || '0' !== trim( $r[2] ) );
$a->set_variation( isset( $r[3] ) && '1' === trim( $r[3] ) );
$attrs[] = $a;
}
return $attrs;
}
private function save_variations( $pid, array $rows ) {
foreach ( $rows as $vid => $d ) {
$vid = (int) $vid;
if ( ! $vid || (int) wp_get_post_parent_id( $vid ) !== (int) $pid ) {
continue;
}
$var = wc_get_product( $vid );
if ( ! $var || ! $var->is_type( 'variation' ) ) {
continue;
}
if ( ! empty( $d['delete'] ) ) {
$var->delete( true );
continue;
}
$sku = isset( $d['sku'] ) ? wc_clean( $d['sku'] ) : '';
if ( $sku && $sku !== $var->get_sku( 'edit' ) && wc_product_has_unique_sku( $vid, $sku ) ) {
$var->set_sku( $sku );
} elseif ( '' === $sku ) {
$var->set_sku( '' );
}
$reg = isset( $d['regular_price'] ) && is_numeric( $d['regular_price'] ) ? wc_format_decimal( $d['regular_price'] ) : '';
$sal = isset( $d['sale_price'] ) && is_numeric( $d['sale_price'] ) ? wc_format_decimal( $d['sale_price'] ) : '';
$var->set_regular_price( $reg );
$var->set_sale_price( '' !== $sal && '' !== $reg && (float) $sal < (float) $reg ? $sal : '' );
if ( isset( $d['stock_quantity'] ) && '' !== $d['stock_quantity'] && is_numeric( $d['stock_quantity'] ) ) {
$var->set_manage_stock( true );
$var->set_stock_quantity( wc_stock_amount( $d['stock_quantity'] ) );
} else {
$var->set_manage_stock( false );
}
$var->set_status( ! empty( $d['enabled'] ) ? 'publish' : 'private' );
$var->save();
}
}
/** Create every missing combination of variation attributes. */
public function ajax_gen_variations( $req ) {
$id = (int) ( $req['id'] ?? 0 );
$p = wc_get_product( $id );
if ( ! $p || ! $p->is_type( 'variable' ) || ! self::can_edit( $id ) ) {
Ajax::fail( __( 'Not allowed.', 'frontglut' ), 403 );
}
$sets = array();
foreach ( $p->get_attributes() as $a ) {
if ( ! $a->get_variation() ) {
continue;
}
$key = $a->is_taxonomy() ? $a->get_name() : sanitize_title( $a->get_name() );
if ( $a->is_taxonomy() ) {
$opts = array();
foreach ( $a->get_terms() as $t ) {
$opts[] = $t->slug;
}
} else {
$opts = $a->get_options();
}
if ( $opts ) {
$sets[ $key ] = $opts;
}
}
if ( ! $sets ) {
Ajax::fail( __( 'Add attributes marked "Used for variations" first, and save.', 'frontglut' ) );
}
$combos = array( array() );
foreach ( $sets as $k => $opts ) {
$next = array();
foreach ( $combos as $c ) {
foreach ( $opts as $o ) {
$next[] = $c + array( $k => $o );
}
}
$combos = $next;
if ( count( $combos ) > 200 ) {
Ajax::fail( __( 'Too many combinations (max 200).', 'frontglut' ) );
}
}
$existing = array();
foreach ( $p->get_children() as $vid ) {
$existing[] = wc_get_product( $vid )->get_attributes();
}
$n = 0;
foreach ( $combos as $c ) {
$dup = false;
foreach ( $existing as $e ) {
if ( $e == $c ) { // phpcs:ignore Universal.Operators.StrictComparisons
$dup = true;
break;
}
}
if ( $dup ) {
continue;
}
$var = new \WC_Product_Variation();
$var->set_parent_id( $id );
$var->set_attributes( $c );
$var->set_status( 'publish' );
$var->save();
$n++;
}
\WC_Product_Variable::sync( $id );
/* translators: %s: value. */
return array( 'message' => sprintf( _n( '%d variation created.', '%d variations created.', $n, 'frontglut' ), $n ) );
}
public function ajax_add_variation( $req ) {
$id = (int) ( $req['id'] ?? 0 );
$p = wc_get_product( $id );
if ( ! $p || ! $p->is_type( 'variable' ) || ! self::can_edit( $id ) ) {
Ajax::fail( __( 'Not allowed.', 'frontglut' ), 403 );
}
$var = new \WC_Product_Variation();
$var->set_parent_id( $id );
$var->set_status( 'publish' );
$var->save();
return array( 'message' => __( 'Variation added.', 'frontglut' ) );
}
/* ---------------- list actions ---------------- */
private function guard_edit( $id ) {
if ( ! self::can_edit( (int) $id ) ) {
Ajax::fail( __( 'Not allowed.', 'frontglut' ), 403 );
}
return wc_get_product( (int) $id );
}
public function ajax_trash( $req ) {
$p = $this->guard_edit( $req['id'] ?? 0 );
$p->delete( false );
Audit::log( 'product_trashed', (string) $p->get_id() );
return array( 'message' => __( 'Moved to trash.', 'frontglut' ) );
}
public function ajax_restore( $req ) {
$this->guard_edit( $req['id'] ?? 0 );
wp_untrash_post( (int) $req['id'] );
return array( 'message' => __( 'Restored.', 'frontglut' ) );
}
public function ajax_delete( $req ) {
$p = $this->guard_edit( $req['id'] ?? 0 );
if ( 'trash' !== $p->get_status() ) {
Ajax::fail( __( 'Trash it first.', 'frontglut' ) );
}
$p->delete( true );
return array( 'message' => __( 'Deleted permanently.', 'frontglut' ) );
}
public function ajax_duplicate( $req ) {
$p = $this->guard_edit( $req['id'] ?? 0 );
if ( ! class_exists( 'WC_Admin_Duplicate_Product' ) ) {
include_once WC_ABSPATH . 'includes/admin/class-wc-admin-duplicate-product.php';
}
if ( ! $this->under_limit( get_current_user_id() ) ) {
Ajax::fail( __( 'You reached the product limit for your role.', 'frontglut' ) );
}
$dup = ( new \WC_Admin_Duplicate_Product() )->product_duplicate( $p );
if ( ! self::is_manager() ) {
wp_update_post( array( 'ID' => $dup->get_id(), 'post_author' => get_current_user_id() ) );
}
return array( 'message' => __( 'Product duplicated as a draft.', 'frontglut' ), 'redirect' => Dashboard::url( 'products', array( 'a' => 'edit', 'id' => $dup->get_id() ) ) );
}
public function ajax_quick( $req ) {
$p = $this->guard_edit( $req['id'] ?? 0 );
if ( isset( $req['name'] ) && '' !== trim( $req['name'] ) ) {
$p->set_name( sanitize_text_field( $req['name'] ) );
}
if ( ! $p->is_type( 'variable' ) && ! $p->is_type( 'grouped' ) ) {
$reg = isset( $req['regular_price'] ) && is_numeric( $req['regular_price'] ) ? wc_format_decimal( $req['regular_price'] ) : '';
$sal = isset( $req['sale_price'] ) && is_numeric( $req['sale_price'] ) ? wc_format_decimal( $req['sale_price'] ) : '';
if ( '' !== $sal && '' !== $reg && (float) $sal >= (float) $reg ) {
Ajax::fail( __( 'Sale price must be lower than the regular price.', 'frontglut' ) );
}
$p->set_regular_price( $reg );
$p->set_sale_price( $sal );
}
if ( $p->get_manage_stock() && isset( $req['stock_quantity'] ) && '' !== $req['stock_quantity'] ) {
$p->set_stock_quantity( wc_stock_amount( $req['stock_quantity'] ) );
}
if ( ! empty( $req['status'] ) ) {
$st = sanitize_key( $req['status'] );
$ok = self::is_manager() ? array( 'publish', 'pending', 'draft', 'private' ) : array( 'draft', 'pending', 'publish' === Settings::get( 'products.default_status' ) ? 'publish' : 'draft' );
if ( in_array( $st, $ok, true ) || $st === $p->get_status() ) {
$p->set_status( $st );
}
}
$p->save();
return array( 'message' => __( 'Product updated.', 'frontglut' ) );
}
public function ajax_bulk( $req ) {
$ids = array_filter( array_map( 'absint', explode( ',', (string) ( $req['ids'] ?? '' ) ) ) );
$op = sanitize_key( $req['value'] ?? '' );
$n = 0;
foreach ( $ids as $id ) {
if ( ! self::can_edit( $id ) ) {
continue;
}
$p = wc_get_product( $id );
if ( ! $p ) {
continue;
}
switch ( $op ) {
case 'publish':
if ( self::is_manager() || 'publish' === Settings::get( 'products.default_status' ) ) {
$p->set_status( 'publish' );
} else {
continue 2;
}
break;
case 'draft':
$p->set_status( 'draft' );
break;
case 'trash':
$p->delete( false );
$n++;
continue 2;
case 'feature':
$p->set_featured( true );
break;
case 'unfeature':
$p->set_featured( false );
break;
case 'instock':
case 'outofstock':
if ( $p->get_manage_stock() ) {
continue 2;
}
$p->set_stock_status( $op );
break;
default:
continue 2;
}
$p->save();
$n++;
}
Audit::log( 'products_bulk', $op, (string) $n );
/* translators: %s: value. */
return array( 'message' => sprintf( _n( '%d product updated.', '%d products updated.', $n, 'frontglut' ), $n ) );
}
/* ---------------- CSV ---------------- */
private static function csv_cell( $v ) {
$v = (string) $v;
return ( '' !== $v && in_array( $v[0], array( '=', '+', '-', '@', "\t", "\r" ), true ) ) ? "'" . $v : $v;
}
public function ajax_export() {
$ids = $this->query_ids( array( 'limit' => 2000 ) );
nocache_headers();
header( 'Content-Type: text/csv; charset=utf-8' );
header( 'Content-Disposition: attachment; filename="products-' . gmdate( 'Ymd' ) . '.csv"' );
$o = fopen( 'php://output', 'w' );
fputcsv( $o, array( 'id', 'type', 'name', 'sku', 'regular_price', 'sale_price', 'stock_quantity', 'stock_status', 'status', 'categories', 'tags', 'short_description', 'description', 'images' ) );
foreach ( $ids as $id ) {
$p = wc_get_product( $id );
if ( ! $p ) {
continue;
}
$imgs = array_filter( array_merge( array( $p->get_image_id() ), $p->get_gallery_image_ids() ) );
fputcsv( $o, array_map( array( __CLASS__, 'csv_cell' ), array( $p->get_id(), $p->get_type(), $p->get_name(), $p->get_sku(), $p->get_regular_price(), $p->get_sale_price(), $p->get_manage_stock() ? $p->get_stock_quantity() : '', $p->get_stock_status(), $p->get_status(), implode( '|', wp_list_pluck( (array) get_the_terms( $id, 'product_cat' ) ?: array(), 'name' ) ), implode( '|', wp_list_pluck( (array) get_the_terms( $id, 'product_tag' ) ?: array(), 'name' ) ), $p->get_short_description(), $p->get_description(), implode( '|', array_map( 'wp_get_attachment_url', $imgs ) ) ) ) );
}
fclose( $o ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- php://output / php://temp stream, not a file on disk.
exit;
}
private function import_screen() {
$h = Ui::header( __( 'Import products', 'frontglut' ), Ui::btn( __( '← Products', 'frontglut' ), Dashboard::url( 'products' ) ) );
$h .= '' . esc_html__( 'Upload a CSV with the columns from the export (id, type, name, sku, regular_price, sale_price, stock_quantity, stock_status, status, categories, tags, short_description, description). Rows with an existing id or SKU are updated, others are created. Max 500 rows.', 'frontglut' ) . '
';
$h .= Ui::form_open( 'product_import', array( 'after' => 'reload', 'extra' => 'enctype="multipart/form-data"' ) ) . '
' . Ui::form_close( __( 'Import', 'frontglut' ) ) . '
';
return $h;
}
public function ajax_import( $req ) {
// The CSV is only read (never stored), so it does not go through the media library.
if ( ! check_ajax_referer( 'frontglut_ajax', '_frontglut_nonce', false ) || ! Caps::can( 'frontglut_manage_products' ) ) {
Ajax::fail( __( 'You are not allowed to do that.', 'frontglut' ), 403 );
}
$csv = Ajax::file( 'csv' );
$tmp = $csv && isset( $csv['tmp_name'] ) && is_string( $csv['tmp_name'] ) ? $csv['tmp_name'] : ''; // Server-generated temp path, checked with is_uploaded_file().
$name = $csv && isset( $csv['name'] ) && is_string( $csv['name'] ) ? sanitize_file_name( wp_unslash( $csv['name'] ) ) : '';
$size = $csv && isset( $csv['size'] ) ? absint( $csv['size'] ) : 0;
if ( '' === $tmp || ! is_uploaded_file( $tmp ) ) {
Ajax::fail( __( 'No file received.', 'frontglut' ) );
}
$check = wp_check_filetype( $name, array( 'csv' => 'text/csv' ) );
if ( 'csv' !== $check['ext'] || $size > 2 * MB_IN_BYTES ) {
Ajax::fail( __( 'Upload a CSV under 2 MB.', 'frontglut' ) );
}
$fh = new \SplFileObject( $tmp, 'r' ); // Read row by row; the uploaded temp file is never stored.
$fh->setFlags( \SplFileObject::READ_CSV | \SplFileObject::SKIP_EMPTY | \SplFileObject::READ_AHEAD );
$head = $fh->eof() ? false : $fh->fgetcsv();
if ( ! $head || ! in_array( 'name', $head, true ) ) {
Ajax::fail( __( 'The CSV needs a header row with a "name" column.', 'frontglut' ) );
}
$head = array_map( 'sanitize_key', array_map( 'trim', $head ) );
$new = $upd = $skip = 0;
$row = 0;
while ( ! $fh->eof() && $row < 500 ) {
$line = $fh->fgetcsv();
if ( ! is_array( $line ) || array( null ) === $line ) {
continue;
}
$row++;
$d = array_combine( $head, array_pad( array_slice( $line, 0, count( $head ) ), count( $head ), '' ) );
foreach ( $d as $ck => $cv ) {
$d[ $ck ] = in_array( $ck, array( 'description', 'short_description' ), true ) ? wp_kses_post( (string) $cv ) : sanitize_text_field( (string) $cv );
}
$p = null;
if ( ! empty( $d['id'] ) && self::can_edit( (int) $d['id'] ) ) {
$p = wc_get_product( (int) $d['id'] );
} elseif ( ! empty( $d['sku'] ) ) {
$sid = wc_get_product_id_by_sku( $d['sku'] );
if ( $sid && self::can_edit( $sid ) ) {
$p = wc_get_product( $sid );
} elseif ( $sid ) {
$skip++;
continue;
}
}
if ( ! $p ) {
if ( ! $this->under_limit( get_current_user_id() ) ) {
$skip++;
continue;
}
$type = isset( $d['type'] ) && 'external' === $d['type'] ? 'WC_Product_External' : 'WC_Product_Simple';
$p = new $type();
$p->set_status( self::is_manager() ? 'draft' : Settings::get( 'products.default_status', 'pending' ) );
$is_new = true;
} else {
$is_new = false;
}
if ( '' === trim( (string) ( $d['name'] ?? '' ) ) && $is_new ) {
$skip++;
continue;
}
if ( ! empty( $d['name'] ) ) {
$p->set_name( sanitize_text_field( ltrim( $d['name'], "'" ) ) );
}
if ( ! $p->is_type( 'variable' ) && ! $p->is_type( 'grouped' ) ) {
if ( isset( $d['regular_price'] ) && is_numeric( $d['regular_price'] ) ) {
$p->set_regular_price( wc_format_decimal( $d['regular_price'] ) );
}
if ( isset( $d['sale_price'] ) ) {
$p->set_sale_price( is_numeric( $d['sale_price'] ) ? wc_format_decimal( $d['sale_price'] ) : '' );
}
}
if ( ! empty( $d['sku'] ) && wc_product_has_unique_sku( $p->get_id(), $d['sku'] ) ) {
$p->set_sku( wc_clean( $d['sku'] ) );
}
if ( isset( $d['stock_quantity'] ) && is_numeric( $d['stock_quantity'] ) ) {
$p->set_manage_stock( true );
$p->set_stock_quantity( wc_stock_amount( $d['stock_quantity'] ) );
} elseif ( ! empty( $d['stock_status'] ) && in_array( $d['stock_status'], array( 'instock', 'outofstock', 'onbackorder' ), true ) && ! $p->get_manage_stock() ) {
$p->set_stock_status( $d['stock_status'] );
}
if ( isset( $d['short_description'] ) ) {
$p->set_short_description( wp_kses_post( $d['short_description'] ) );
}
if ( isset( $d['description'] ) ) {
$p->set_description( wp_kses_post( $d['description'] ) );
}
if ( ! empty( $d['status'] ) && self::is_manager() && in_array( $d['status'], array( 'publish', 'draft', 'pending', 'private' ), true ) ) {
$p->set_status( $d['status'] );
}
$pid = $p->save();
if ( ! empty( $d['categories'] ) ) {
$ids = array();
foreach ( array_filter( array_map( 'trim', explode( '|', $d['categories'] ) ) ) as $cn ) {
$t = get_term_by( 'name', $cn, 'product_cat' );
if ( $t ) {
$ids[] = $t->term_id;
} elseif ( current_user_can( 'manage_product_terms' ) ) {
$ins = wp_insert_term( $cn, 'product_cat' );
if ( ! is_wp_error( $ins ) ) {
$ids[] = $ins['term_id'];
}
}
}
wp_set_object_terms( $pid, $ids, 'product_cat' );
}
if ( ! empty( $d['tags'] ) ) {
wp_set_object_terms( $pid, array_filter( array_map( 'trim', explode( '|', $d['tags'] ) ) ), 'product_tag' );
}
$is_new ? $new++ : $upd++;
}
$fh = null;
Audit::log( 'products_import', '', "$new new, $upd updated" );
/* translators: 1: value, 2: value, 3: value. */
return array( 'message' => sprintf( __( 'Import finished: %1$d created, %2$d updated, %3$d skipped.', 'frontglut' ), $new, $upd, $skip ) );
}
/* ---------------- storefront flyout ---------------- */
public function flyout() {
if ( ! Settings::get( 'products.flyout' ) || ! is_user_logged_in() || ! is_singular( 'product' ) || ! self::can_edit( get_the_ID() ) || Request::route( 'frontglut_embed', 'bool', false ) ) {
return;
}
Assets::need();
echo '✎ ' . esc_html__( 'Edit', 'frontglut' ) . ' ';
}
public function admin_bar( $bar ) {
if ( is_singular( 'product' ) && is_user_logged_in() && self::can_edit( get_the_ID() ) ) {
$bar->add_node( array( 'id' => 'frontglut-edit-product', 'title' => esc_html__( 'Edit in FrontGlut', 'frontglut' ), 'href' => Dashboard::url( 'products', array( 'a' => 'edit', 'id' => get_the_ID() ) ) ) );
}
}
}