__( '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() ) . '
' . ( $p->is_type( 'variable' ) || $p->is_type( 'grouped' ) ? '' : '
' ) . '
get_manage_stock() ? '' : 'placeholder="not managed"' ) . '>
' . '
' . 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 = '
'; $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 .= ''; } 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 .= ''; $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 .= '
' . $tabs . $panes . '
' . $side . '
'; $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 ''; } 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() ) ) ) ); } } }