( $feed_config['name'] == 'Heureka.sk' ) ) && ( $k == 'DELIVERY' ) ) { $delivery = $product->addChild( 'DELIVERY' ); $zbozi_delivery_id = array( 0 => 'CESKA_POSTA_BALIKOVNA', 1 => 'CESKA_POSTA_NA_POSTU', 2 => 'DPD_PICKUP', 3 => 'GEIS_POINT', 4 => 'GLS_PARCELSHOP', 5 => 'PPL_PARCELSHOP', 6 => 'TOPTRANS_DEPO', 7 => 'WEDO_ULOZENKA', 8 => 'ZASILKOVNA', 9 => 'VLASTNI_VYDEJNI_MISTA', 10 => 'CESKA_POSTA', 11 => 'DB_SCHENKER', 12 => 'DPD', 13 => 'DHL', 14 => 'DSV', 15 => 'FOFR', 16 => 'GEBRUDER_WEISS', 17 => 'GEIS', 18 => 'GLS', 19 => 'HDS', 20 => 'WEDO_HOME', 21 => 'MESSENGER', 22 => 'PPL', 23 => 'TNT', 24 => 'TOPTRANS', 25 => 'UPS', 26 => 'FEDEX', 27 => 'RABEN_LOGISTICS', 28 => 'RHENUS', 29 => 'ZASILKOVNA_NA_ADRESU', 30 => 'VLASTNI_PREPRAVA', ); // Parse the WOOSEA_*##-encoded shipping string into structured // delivery methods. Format: "WOOSEA_KEY##value:WOOSEA_KEY##value||..." // where "||" separates shipping methods and ":" separates the fields // within a method. This replaces the earlier positional parsing that // only produced a DELIVERY_PRICE_COD when the "##" count was exactly 7 // (the coincidental 2-method case) and mangled the price otherwise — // standalone local pickup (WC 8.3+ Checkout block) adds address fields // that break that assumption. See issues #973 and #974. $delivery_methods = array(); foreach ( explode( '||', $v ) as $delivery_method_str ) { if ( '' === trim( $delivery_method_str ) ) { continue; } $delivery_method_fields = array(); foreach ( explode( ':', $delivery_method_str ) as $delivery_field_str ) { $delivery_field_parts = explode( '##', $delivery_field_str, 2 ); if ( count( $delivery_field_parts ) === 2 ) { $delivery_method_fields[ $delivery_field_parts[0] ] = $delivery_field_parts[1]; } } if ( ! empty( $delivery_method_fields ) ) { $delivery_methods[] = $delivery_method_fields; } } if ( ! empty( $delivery_methods ) ) { $first_delivery_method = $delivery_methods[0]; // DELIVERY_ID: prefer a recognised carrier code found in the // service name, otherwise fall back to its first token. $delivery_service = $first_delivery_method['WOOSEA_SERVICE'] ?? ''; $delivery_id_value = ''; foreach ( explode( ' ', $delivery_service ) as $delivery_service_token ) { if ( in_array( $delivery_service_token, $zbozi_delivery_id, true ) ) { $delivery_id_value = $delivery_service_token; break; } } if ( '' === $delivery_id_value ) { $delivery_service_tokens = explode( ' ', trim( $delivery_service ) ); $delivery_id_value = $delivery_service_tokens[0]; } // Omit DELIVERY_ID entirely when the method carries no service // name, mirroring how DELIVERY_PRICE_COD is omitted when absent. if ( '' !== $delivery_id_value ) { $delivery->addChild( 'DELIVERY_ID', htmlspecialchars( $delivery_id_value ) ); } // Prepare a delivery price for output: strip currency labels, // escape entities, and trim. Shared by DELIVERY_PRICE and // DELIVERY_PRICE_COD so both go through the same transformation. $prepare_delivery_price = function ( $price ) { return trim( htmlspecialchars( str_replace( array( 'EUR', 'CZK' ), '', $price ) ) ); }; // DELIVERY_PRICE: the first method's price, stripped of currency labels. $delivery->addChild( 'DELIVERY_PRICE', $prepare_delivery_price( $first_delivery_method['WOOSEA_PRICE'] ?? '' ) ); // DELIVERY_PRICE_COD: the second method's price is treated as the // cash-on-delivery price, preserving the historical behaviour where // a second shipping method encoded the COD / pickup price. if ( isset( $delivery_methods[1]['WOOSEA_PRICE'] ) ) { $delivery->addChild( 'DELIVERY_PRICE_COD', $prepare_delivery_price( $delivery_methods[1]['WOOSEA_PRICE'] ) ); } } } elseif ( ( $feed_config['name'] == 'Yandex' ) && ( preg_match( '/picture/i', $k ) ) ) { // do nothing, was added already } elseif ( ( $feed_config['name'] == 'Yandex' ) && ( preg_match( "/$zbozi_nodes/i", $k ) ) ) { $pieces = explode( '_', $k ); $p = 'param'; $productp = $this->add_xml_child_with_escaped_entities( $product, $p, $v ); $productp->addAttribute( 'name', $pieces[1] ); } elseif ( $feed_config['name'] == 'Google Product Review' ) { } elseif ( $feed_config['name'] == 'Vivino' ) { $extra_arr = array( 'ean', 'jan', 'upc', 'producer', 'wine-name', 'appellation', 'vintage', 'country', 'color', 'image', 'description', 'alcohol', 'producer-address', 'importer-address', 'varietal', 'ageing', 'closure', 'winemaker', 'production-size', 'residual-sugar', 'acidity', 'ph', 'contains-milk-allergens', 'contains-egg-allergens', 'non-alcoholic' ); $unit_arr = array( 'production-size', 'residual-sugar', 'acidity' ); if ( in_array( $k, $extra_arr ) ) { if ( ! isset( $product->extras ) ) { $productp = $product->addChild( 'extras' ); } // Add units to it if ( in_array( $k, $unit_arr ) ) { $productk = $this->add_xml_child_with_escaped_entities( $productp, $k, $v ); if ( $k == 'acidity' ) { $productk->addAttribute( 'unit', 'g/l' ); } if ( $k == 'production-size' ) { $productk->addAttribute( 'unit', 'bottles' ); } if ( $k == 'residual-sugar' ) { $productk->addAttribute( 'unit', 'g/l' ); } } else { $productp->$k = $v; } } else { $product->addChild( "$k" ); $product->$k = $v; } } elseif ( $feed_config['name'] == 'Fruugo.nl' ) { $desc_arr = array( 'Language', 'Title', 'Description' ); $price_arr = array( 'Currency', 'NormalPriceWithoutVAT', 'NormalPriceWithVAT', 'VATRate' ); if ( in_array( $k, $desc_arr ) ) { if ( ! isset( $product->Description ) ) { $productd = $product->addChild( 'Description' ); } $productd->$k = $v; } elseif ( in_array( $k, $price_arr ) ) { if ( ! isset( $product->Price ) ) { $productp = $product->addChild( 'Price' ); } $productp->$k = $v; } else { $product->addChild( "$k" ); $product->$k = $v; } } elseif ( $feed_config['name'] == 'Fruugo.co.uk' ) { $desc_arr = array( 'Language', 'Title', 'Description' ); $price_arr = array( 'Currency', 'NormalPriceWithoutVAT', 'NormalPriceWithVAT', 'VATRate' ); if ( in_array( $k, $desc_arr ) ) { if ( ! isset( $product->Description ) ) { $productd = $product->addChild( 'Description' ); } $productd->$k = $v; } elseif ( in_array( $k, $price_arr ) ) { if ( ! isset( $product->Price ) ) { $productp = $product->addChild( 'Price' ); } $productp->$k = $v; } else { $product->addChild( "$k" ); $product->$k = $v; } } elseif ( $feed_config['name'] == 'Pinterest RSS Board' && $k === 'image' ) { if ( ! empty( $v ) ) { $media_content = $product->addChild( 'media:content', '', 'http://search.yahoo.com/mrss/' ); $media_content->addAttribute( 'medium', 'image' ); $media_content->addAttribute( 'url', $v ); } } elseif ( $feed_config['name'] == 'Pinterest RSS Board' && $k === 'guid' ) { if ( ! empty( $v ) ) { $guid = $this->add_xml_child_with_escaped_entities( $product, 'guid', $v ); $guid->addAttribute( 'isPermaLink', 'true' ); } } elseif ( $feed_config['name'] == 'Skroutz' ) { if ( 'skroutz_variations' == $k ) { $this->woosea_create_skroutz_variations( $product, $v ); } else { if ( ! isset( $product->$k ) ) { $product->addChild( $k, htmlspecialchars( $v, ENT_XML1, 'UTF-8' ) ); } } } elseif ( is_object( $product ) && ! isset( $product->$k ) ) { // Check if this field should use CDATA for HTML preservation $use_cdata = $this->should_use_cdata_for_field( $k, $feed_config, $feed ); // Handle Google Shopping namespace for g: prefixed elements if ( ( $feed_config['taxonomy'] == 'google_shopping' ) && ( str_starts_with( $k, 'g:' ) ) ) { $this->add_child_with_cdata( $product, $k, $v, 'http://base.google.com/ns/1.0', $use_cdata ); } else { $this->add_child_with_cdata( $product, $k, $v, '', $use_cdata ); } } } } } /** * Write a single Merkandi product field to the XML element. * * Handles the Merkandi-specific output rules: / wrapped in * CDATA, rendered as a parent element with up to 7 children, * and every other field as a plain (entity-escaped) child. Empty values are * skipped so the feed never contains blank tags. * * @since 13.5.5 * * @param object $product The product XML element. * @param string $k The attribute key (feed tag name). * @param string $v The attribute value. * @return void */ private function woosea_write_merkandi_field( $product, $k, $v ) { if ( ! is_object( $product ) || empty( $k ) ) { return; } // Skip empty values to avoid blank tags (Merkandi rejects empty elements). if ( '' === $v || null === $v ) { return; } // Multiple images: one child per URL, capped at 7. if ( 'photos' === $k ) { if ( isset( $product->photos ) ) { return; } $urls = preg_split( '/\s*,\s*/', $v, -1, PREG_SPLIT_NO_EMPTY ); if ( empty( $urls ) ) { return; } $photos = $product->addChild( 'photos' ); $count = 0; foreach ( $urls as $url ) { if ( $count >= self::MERKANDI_MAX_PHOTOS ) { break; } $this->add_xml_child_with_escaped_entities( $photos, 'photoUrl', $url ); ++$count; } return; } // Name and description must be wrapped in CDATA. if ( 'name' === $k || 'description' === $k ) { $this->add_child_with_cdata( $product, $k, $v, '', true ); return; } // Any other (scalar) field as a plain, entity-escaped child. if ( ! isset( $product->$k ) ) { $this->add_xml_child_with_escaped_entities( $product, $k, $v ); } } /** * Add a tiered block to a Merkandi product. * * Renders each tier as a child of , with nested , * and sub-tags as required by the Merkandi spec. When this is * used the flat tag is omitted by the caller. * * @since 13.5.5 * * @param object $product The product XML element. * @param array $tiers List of tiers: array( 'qFrom' => int, 'qTo' => int, 'price' => string ). * @return void */ private function woosea_add_merkandi_pricing( $product, $tiers ) { if ( ! is_object( $product ) || isset( $product->pricing ) ) { return; } $pricing = $product->addChild( 'pricing' ); foreach ( $tiers as $tier ) { if ( ! is_array( $tier ) || ! isset( $tier['price'] ) || '' === $tier['price'] ) { continue; } $tier_node = $pricing->addChild( 'price' ); if ( isset( $tier['qFrom'] ) && '' !== $tier['qFrom'] ) { $this->add_xml_child_with_escaped_entities( $tier_node, 'qFrom', (string) $tier['qFrom'] ); } if ( isset( $tier['qTo'] ) && '' !== $tier['qTo'] ) { $this->add_xml_child_with_escaped_entities( $tier_node, 'qTo', (string) $tier['qTo'] ); } $this->add_xml_child_with_escaped_entities( $tier_node, 'price', (string) $tier['price'] ); } } /** * Check if a field should be wrapped in CDATA for the given feed. * * @since 13.5.1 * * @param string $field_name The field name to check. * @param array $feed_config The feed configuration array. * @param object $feed The feed object. * @return bool True if field should use CDATA, false otherwise. */ private function should_use_cdata_for_field( $field_name, $feed_config, $feed ) { // Define fields that may contain HTML content $html_content_fields = array( // Google Shopping & Facebook DRM (with g: namespace). 'g:description', 'g:structured_description', // Bing Shopping (without namespace prefix). 'description', 'short_description', // Facebook DRM. 'rich_text_description', ); /** * Filter the fields that should use CDATA wrapping. * * @since 13.5.1 * * @param array $html_content_fields The fields that should use CDATA. * @param array $feed_config The feed configuration array. * @param object $feed The feed object. */ $html_content_fields = apply_filters( 'adt_product_feed_cdata_fields', $html_content_fields, $feed_config, $feed ); if ( ! in_array( $field_name, $html_content_fields, true ) ) { return false; } // Check if the platform/channel allows HTML formatting $platforms_allowing_html = array( 'bing_shopping', 'bing_shopping_promotions', 'facebook_drm', 'google_shopping', 'google_drm', 'google_dsa', 'google_local', 'google_local_products', 'google_product_review', 'google_shopping_promotions', ); /** * Filter the platforms that support HTML in descriptions. * * @since 13.5.1 * * @param array $platforms_allowing_html The platforms that support HTML. * @param array $feed_config The feed configuration array. * @param object $feed The feed object. */ $platforms_allowing_html = apply_filters( 'adt_product_feed_cdata_platforms', $platforms_allowing_html, $feed_config, $feed ); $channel = isset( $feed_config['taxonomy'] ) ? $feed_config['taxonomy'] : ''; return in_array( $channel, $platforms_allowing_html, true ); } /** * Add a child element with CDATA support to the XML. * * @since 13.5.1 * * @param object $parent The parent XML element. * @param string $name The name of the child element. * @param string $value The value to add (will be wrapped in CDATA if needed). * @param string $namespace Optional namespace URI. * @param bool $use_cdata Whether to wrap value in CDATA. * @return object|null The created child element or null on failure. */ private function add_child_with_cdata( $parent, $name, $value, $namespace = '', $use_cdata = false ) { if ( ! is_object( $parent ) ) { return null; } if ( ! $use_cdata ) { // Standard behavior - escape HTML entities if ( ! empty( $namespace ) ) { return $parent->addChild( $name, htmlspecialchars( $value, ENT_XML1, 'UTF-8' ), $namespace ); } else { return $parent->addChild( $name, htmlspecialchars( $value, ENT_XML1, 'UTF-8' ) ); } } // CDATA behavior - preserve HTML within CDATA section // SimpleXML doesn't support CDATA natively, so we need to use DOM try { // Create the child element first (empty) if ( ! empty( $namespace ) ) { $child = $parent->addChild( $name, '', $namespace ); } else { $child = $parent->addChild( $name ); } // Get the DOM element from SimpleXML $dom_element = dom_import_simplexml( $child ); $dom_document = $dom_element->ownerDocument; // Create CDATA section with the value $cdata = $dom_document->createCDATASection( $value ); // Append CDATA to the element $dom_element->appendChild( $cdata ); return $child; } catch ( \Exception $e ) { // Fallback to standard behavior if CDATA fails if ( ! empty( $namespace ) ) { return $parent->addChild( $name, htmlspecialchars( $value, ENT_XML1, 'UTF-8' ), $namespace ); } else { return $parent->addChild( $name, htmlspecialchars( $value, ENT_XML1, 'UTF-8' ) ); } } } /** * Create Skroutz variations * * @param object $product Product object * @param array $variation_data Variation data */ private function woosea_create_skroutz_variations( $product, $variation_data ) { $change_skroutz_variation_child = apply_filters( 'woosea_change_skroutz_variation_child', array( 'id' => 'variationid', ) ); $skroutz_variations_attributes = apply_filters( 'woosea_skroutz_variations_attributes', array( 'variationid', 'link', 'availability', 'size', 'quantity', 'manufacturersku', 'ean', 'price_with_vat', ) ); $product->addChild( 'variations' ); foreach ( $variation_data as $variation ) { if ( ! is_array( $variation ) || empty( $variation ) ) { continue; } $child_variation = $product->variations->addChild( 'variation' ); foreach ( $variation as $variation_key => $variation_value ) { $child_variation_key = $variation_key; $child_variation_value = $variation_value; if ( in_array( $variation_key, array_keys( $change_skroutz_variation_child ) ) ) { $child_variation_key = $change_skroutz_variation_child[ $variation_key ]; } if ( in_array( $child_variation_key, $skroutz_variations_attributes ) ) { $child_variation->addChild( $child_variation_key ); $child_variation->$child_variation_key = $child_variation_value; } } } } /** * Actual creation of CSV/TXT feed * Returns relative and absolute file path */ public function woosea_create_csvtxt_feed( $products, $feed, $header ) { $path = $feed->get_file_dir_path(); $file = $feed->get_temp_file_path(); $external_file = $feed->get_file_url(); // Check if directory in uploads exists, if not create one if ( ! file_exists( $path ) ) { wp_mkdir_p( $path ); } // Check if file exists, if it does: delete it first so we can create a new updated one if ( ( $header == 'true' ) && $feed->is_first_write_of_run() && file_exists( $file ) ) { @unlink( $file ); } // Check if there is a channel feed class that we need to use $fields = $feed->get_channel( 'fields' ); if ( empty( $fields ) ) { return $external_file; } // Load channel attributes if needed $channel_attributes = $this->load_channel_attributes($fields); // Append or write to file $fp = fopen( $file, 'a+' ); // Set proper UTF encoding BOM for CSV files. // Pinterest's CSV parser reads the BOM as part of the first column header // (e.g. "\xEF\xBB\xBFid" instead of "id"), so it cannot find the required // "id" field and aborts ingestion with Error 9156. Exclude it like Fruugo. if ( $header == 'true' && ! preg_match( '/fruugo|pinterest/i', $fields ) ) { fputs( $fp, $bom = chr( 0xEF ) . chr( 0xBB ) . chr( 0xBF ) ); } // Set delimiter $csv_delimiter = ($feed->delimiter == 'tab') ? "\t" : $feed->delimiter; // Process each product row foreach ( $products as $row ) { foreach ( $row as $k => $v ) { $pieces = explode( "','", $v ); if ( empty( $pieces ) ) { $pieces = array_map( 'trim', $pieces ); fputcsv( $fp, $pieces, $csv_delimiter, '"', '\\' ); continue; } // Special handling for Google Local feeds if ( $fields == 'google_local' && $header == 'false' ) { $this->process_google_local_feed($fp, $pieces, $feed, $fields, $channel_attributes, $header, $csv_delimiter); continue; } // Process standard feed format $csv_line = $this->prepare_csv_line($pieces, $fields, $channel_attributes, $header, $feed); fputcsv( $fp, $csv_line, $csv_delimiter, '"', '\\' ); } } // Close the file fclose( $fp ); // Return external location of feed return $external_file; } /** * Load channel attributes for a specific feed type * * @param string $fields The feed field type * @throws \Exception If invalid channel name detected * @return array Channel attributes */ private function load_channel_attributes($fields) { $channel_attributes = array(); if ( $fields != 'standard' && $fields != 'customfeed' ) { if ( ! class_exists( 'WooSEA_' . $fields ) ) { // Sanitize channel name to prevent path traversal attacks $channel_name = sanitize_file_name( $fields ); // Additional validation: only allow alphanumeric characters and underscores if ( ! preg_match( '/^[a-zA-Z0-9_]+$/', $channel_name ) ) { throw new \Exception( 'Invalid channel name detected: ' . $fields ); } $channel_file_path = plugin_dir_path( __FILE__ ) . '/channels/class-' . $channel_name . '.php'; // Verify the file is within the channels directory (prevent directory traversal) $real_path = realpath( $channel_file_path ); $channels_dir = realpath( plugin_dir_path( __FILE__ ) . '/channels' ); if ( $real_path && $channels_dir && str_starts_with( $real_path, $channels_dir ) && file_exists( $real_path ) ) { require $real_path; $channel_class = 'WooSEA_' . $channel_name; $channel_attributes = $channel_class::get_channel_attributes(); } } } return $channel_attributes; } /** * Process Google Local feed format * * @param resource $fp File pointer * @param array $pieces CSV line pieces * @param object $feed Feed object * @param string $fields Feed field type * @param array $channel_attributes Channel attributes * @param string $header Whether this is a header row * @param string $csv_delimiter CSV delimiter */ private function process_google_local_feed($fp, $pieces, $feed, $fields, $channel_attributes, $header, $csv_delimiter) { // Get the store codes $stores_local = ''; foreach ( $feed->attributes as $attr ) { if ( isset( $attr['attribute'] ) && $attr['attribute'] == 'g:store_code' ) { $stores_local = $attr['mapfrom']; break; } } // Process each piece $pieces = $this->prepare_csv_line($pieces, $fields, $channel_attributes, $header, $feed); if ( ! empty( $stores_local ) ) { $store_ids = explode( '|', $stores_local ); // If we have multiple store IDs, create a line for each store if ( count( $store_ids ) > 1 ) { foreach ( $store_ids as $store_value ) { if ( ! empty( $store_value ) ) { // Replace the store code in position 1 $pieces_copy = $pieces; $pieces_copy[1] = $store_value; fputcsv( $fp, $pieces_copy, $csv_delimiter, '"', '\\' ); } } } else { // Single store code case $pieces[1] = trim($stores_local); fputcsv( $fp, $pieces, $csv_delimiter, '"', '\\' ); } } else { // No store code specified fputcsv( $fp, $pieces, $csv_delimiter, '"', '\\' ); } } /** * Prepare CSV line by processing each piece * * @param array $pieces CSV line pieces * @param string $fields Feed field type * @param array $channel_attributes Channel attributes * @param string $header Whether this is a header row * @param object $feed Feed object * @return array Processed CSV line */ private function prepare_csv_line($pieces, $fields, $channel_attributes, $header, $feed) { $csv_line = array(); foreach ( $pieces as $k_inner => $v ) { // For CSV fileformat the keys need to get stripped of the g: if ( $header === 'true' && in_array( $feed->file_format, array( 'csv', 'txt', 'tsv', 'csv.gz' ), true ) ) { $v = str_replace( 'g:', '', $v ); } // Clean up the value $v = trim( $v, "\"'" ); $csv_line[] = $v; } return $csv_line; } /** * Get products that are eligable for adding to the file. * * @since 13.3.5 Updated the parameters to feed id. * @since 13.4.1 Add offset and batch size parameters. * @since 13.5.7 Batches page with a keyset cursor ($feed->batch_cursor) instead of * LIMIT/OFFSET; $offset is only a fallback for a run started before * the cursor existed. * * @param Product_Feed $feed The product feed instance. * @param int $offset The offset of the batch. Only used as a fallback, on either of the * two triggers that disable the cursor: the feed has no batch cursor * yet (first batch of a run, or a run started before the cursor * existed), or a filter changed the query's orderby away from the * ( post_date DESC, ID DESC ) order the cursor is built on. * @param int $batch_size The batch size. */ public function woosea_get_products( $feed, $offset = 0, $batch_size = 0 ) { if ( ! Product_Feed_Helper::is_a_product_feed( $feed ) ) { return; } $file_format = $feed->file_format; $feed_channel = $feed->channel; $feed_mappings = $feed->mappings; // Resolve once per batch, before anything touches the file: the first batch // of a run starts the feed file - truncating whatever the last run left and // writing the header - and every later batch appends to it. Asking here // rather than only inside the writers means the answer is settled even for // the writers that ask behind a file_exists() check, which on the very // first batch would otherwise skip the question and leave the file // unclaimed for the next batch to truncate. $starts_feed_file = $feed->is_first_write_of_run(); /** * Filter the feed attributes before processing products. * This allows modification of the attributes array to exclude or modify specific attributes. * * @since 13.5.0 * @param array $feed_attributes The feed attributes array. * @return array */ $feed_attributes = apply_filters( 'adt_feed_get_attributes', $feed->attributes ); $feed_rules = $feed->feed_rules; $feed_filters = $feed->feed_filters; if ( empty( $feed_channel ) ) { return false; } // Set class properties. $this->file_format = $file_format; /** * Action hook before getting products. * * @since 13.3.7 * * @param Product_Feed $feed The product feed instance. */ do_action( 'woosea_before_get_products', $feed ); /** * Check if the [attributes] array in the project_config is of expected format. * For channels that have mandatory attribute fields (such as Google shopping) we need to rebuild the [attributes] array * Only add fields to the file that the user selected * Construct header line for CSV ans TXT files, for XML create the XML root and header */ $products = array(); if ( $file_format == 'jsonl' || $file_format == 'jsonl.gz' ) { // Initialize JSONL feed (no header needed for JSONL format). $jsonl_writer = \AdTribes\PFP\Classes\Feed_Writers\Feed_Writer_JSONL::instance(); $file = $jsonl_writer->write_feed( array(), $feed, true ); } elseif ( $file_format != 'xml' ) { // The header row belongs to the batch that starts the file, which is the // same batch that truncates it - keep both decisions on one predicate so // a truncated file can never end up without its header. if ( ! empty( $feed_attributes ) && $starts_feed_file ) { $attr = ''; foreach ( $feed_attributes as $feed_attribute ) { $attr .= "'" . $feed_attribute['attribute'] . "'"; // If last element, do not add a comma. if ( end( $feed_attributes ) !== $feed_attribute ) { $attr .= ','; } } /** * Filter to allow manipulation of CSV header row before it is written to the feed. * This allows channel-specific handling of header attributes in CSV format. * * @since 13.4.9 * * @param string $attr The CSV header row string * @param array $feed_attributes The feed attributes configuration * @param object $feed The feed object * @return string Modified CSV header row */ $attr = apply_filters( 'adt_product_feed_csv_header', $attr, $feed_attributes, $feed ); // Somehow it requires an array, we will do this for now until we refactor the file writing process. $file = $this->woosea_create_csvtxt_feed( array( array( $attr ) ), $feed, 'true' ); } } else { $products[] = array(); $file = $this->woosea_create_xml_feed( $products, $feed, 'true' ); } $xml_piece = array(); // Get taxonomies $no_taxonomies = array( 'element_category', 'template_category', 'portfolio_category', 'portfolio_skills', 'portfolio_tags', 'faq_category', 'slide-page', 'category', 'post_tag', 'nav_menu', 'link_category', 'post_format', 'product_type', 'product_visibility', 'product_cat', 'product_shipping_class', 'product_tag' ); $taxonomies = get_taxonomies(); $diff_taxonomies = array_diff( $taxonomies, $no_taxonomies ); // Check if we need to get just products or also product variations if ( $feed->include_product_variations ) { $post_type = array( 'product', 'product_variation' ); } else { $post_type = array( 'product' ); } // Check shipping currency location $feed->ship_suffix = false; if ( ! empty( $feed_attributes ) ) { foreach ( $feed_attributes as $attr_key => $attr_value ) { if ( $attr_value['mapfrom'] == 'shipping' ) { if ( ! empty( $attr_value['suffix'] ) ) { $feed->ship_suffix = true; } } } } // Pinteres RSS feeds need different sorting if ( $feed_channel['fields'] == 'pinterest_rss' ) { $orderby = 'ASC'; } else { $orderby = 'DESC'; } // Get Orders if ( $feed->utm_total_product_orders_lookback && $feed->utm_total_product_orders_lookback > 0 ) { $allowed_product_orders = \AdTribes\PFP\Classes\Orders::get_orders( $feed ); } unset( $prods ); // Preview mode configuration: Query in batches until we find enough matching products $is_preview_mode = false; $preview_target_count = 0; $preview_found_count = 0; $preview_batch_size = 50; $preview_max_queries = 20; // Prevent infinite loops (50 * 20 = 1000 products max) $preview_query_count = 0; if ( $feed->create_preview ) { $is_preview_mode = true; $preview_target_count = apply_filters( 'adt_product_feed_preview_products', 5, $feed ); $preview_batch_size = apply_filters( 'adt_product_feed_preview_batch_size', 50, $feed ); $preview_max_queries = apply_filters( 'adt_product_feed_preview_max_queries', 20, $feed ); $posts_per_page = $preview_batch_size; } else { $posts_per_page = $batch_size; } // SHIPPING ZONES IS BIG, TAKES TOO MUCH MEMORY $shipping_zones = $this->woosea_get_shipping_zones(); // Check if total_product_orders is needed in the feed (attributes, filters, or rules) $is_total_product_orders_mapped = \AdTribes\PFP\Classes\Orders::is_total_product_orders_mapped( $feed ); // Tax class options. $tax_class_options = function_exists( 'wc_get_product_tax_class_options' ) ? wc_get_product_tax_class_options() : array(); // Minimum image size validation state — Google Shopping and Facebook Catalog enforce 500x500. // Resolved once per feed run since all inputs are loop-invariant. $image_size_validation_in_effect = \AdTribes\PFP\Helpers\Image_Size_Validator::applies_to_channel( $feed_channel ); $image_size_min_dimensions = $image_size_validation_in_effect ? \AdTribes\PFP\Helpers\Image_Size_Validator::get_min_dimensions( $feed_channel ) : array( 'width' => 0, 'height' => 0 ); // Batch pagination cursor (keyset pagination). // // The catalogue is walked in (post_date DESC, ID DESC) order and every batch // resumes strictly after the last row of the previous one. LIMIT/OFFSET is not // safe here: post_date is not unique - variations created in bulk share it down // to the second - so rows with an equal post_date form tie groups whose internal // order is not guaranteed to be the same in two separate queries. A tie group // straddling a batch boundary could then be returned by both batches (duplicated // in the feed) or by neither (silently missing from the feed). // // Only a well-formed cursor is honoured; anything else falls back to the offset // below, so a run already in flight when the plugin was updated still resumes // from where it was instead of starting over. // // Preview mode never seeds the cursor from the feed and never persists it back, // but it is not cursor-free: the local $batch_cursor is advanced after every // query below, so a multi-query preview pages its 2nd and later iterations by // cursor rather than by offset. That is deliberate - it gives preview the same // tie-group correctness as a real run - and it is why the ! $use_cursor offset // branch stops firing once a preview goes past its first query. $batch_cursor = array(); if ( ! $is_preview_mode && Product_Data::is_valid_cursor( $feed->batch_cursor ) ) { $batch_cursor = array( 'date' => (string) $feed->batch_cursor['date'], 'id' => (int) $feed->batch_cursor['id'], ); } // The order the cursor is built on. A cursor only means anything while the query // still sorts by these columns, which is checked after the query args filter. $keyset_orderby = array( 'date' => 'DESC', 'ID' => 'DESC', ); // Main product query loop - will iterate multiple times in preview mode if needed do { // Construct WP query $wp_query = array( 'post_type' => $post_type, 'posts_per_page' => $posts_per_page, 'post_status' => 'publish', // A total order: post_date on its own is not unique, so ID breaks the // ties and makes the row order identical for every query of the run. 'orderby' => $keyset_orderby, // Redundant while orderby carries its own direction, but kept so a filter // that replaces orderby with a single column still sorts descending. 'order' => 'desc', 'fields' => 'ids', 'no_found_rows' => true, 'cache_results' => false, 'update_post_term_cache' => false, 'update_post_meta_cache' => false, 'suppress_filters' => false, 'custom_query' => 'adt_published_products_and_variations', // Custom flag to trigger the filter 'post_password' => '', // Resume point for this batch. @see Product_Data::apply_batch_cursor(). Product_Data::BATCH_CURSOR_QUERY_ARG => $batch_cursor, ); if ( $offset > 0 ) { $wp_query['offset'] = $offset; } /** * Filter the WP_Query arguments for getting products. * * @since 13.3.7 * * @param array $wp_query The WP_Query arguments. * @param Product_Feed $feed The product feed instance. */ $wp_query = apply_filters( 'adt_product_feed_get_products_query_args', $wp_query, $feed ); // Exactly one pagination mechanism may be in play. The cursor skips rows by // their (post_date, ID) sort key, so it is only sound while the query is still // sorted that way: if a filter re-ordered the query, or there is no cursor yet // (first batch, or a run that started before this file was updated), fall back // to LIMIT/OFFSET. // // The sort directions are upper-cased before comparing because WP_Query treats // 'desc' and 'DESC' as the same thing - a filter that rebuilds the same order in // lowercase must keep the cursor, not silently revert the run to OFFSET paging. // The column keys are compared as they are, since WP_Query matches those against // its own whitelist strictly ('id' is not 'ID' to it either). // // Only scalars are cast: the cast itself is load-bearing (it keeps PHP 8.1 from // deprecating strtoupper( null )), but a filter that set a direction to an array // would make it emit an "Array to string conversion" warning on the way to the // fallback. Left as-is, a non-scalar simply fails the match, quietly. $query_orderby = isset( $wp_query['orderby'] ) ? $wp_query['orderby'] : null; $use_cursor = ! empty( $batch_cursor ) && is_array( $query_orderby ) && array_map( static function ( $direction ) { return is_scalar( $direction ) ? strtoupper( (string) $direction ) : $direction; }, $query_orderby ) === $keyset_orderby; // A cursor that exists but cannot be used means a filter re-ordered the query and // this batch drops back to the LIMIT/OFFSET paging the cursor replaced. Say so: // the symptom - rows duplicated or missing around a batch boundary - is otherwise // invisible, and looks identical to "no cursor yet" from the outside. // // Rate-limited to one entry per feed per hour. The trigger is a filter, so it // holds for every batch of the run: logging it per batch would put N-1 identical // lines in front of everything else in the log. The transient outlives a single // run on purpose - this describes a standing misconfiguration, not an event. // // The transient rate-limits the log entry, but reading it is itself two // uncached queries without a persistent object cache, and this block can run // more than once per process: Action Scheduler may work through several of a // feed's batches in one request, and a preview pages in a loop. One check per // feed per process is enough - after the first, this process either wrote the // transient or found it already set. static $cursor_notice_checked = array(); if ( ! empty( $batch_cursor ) && ! $use_cursor && empty( $cursor_notice_checked[ $feed->id ] ) && 'yes' === get_option( 'adt_enable_logging', 'no' ) ) { $cursor_notice_checked[ $feed->id ] = true; $cursor_notice_key = 'adt_cursor_disabled_notice_' . $feed->id; if ( false === get_transient( $cursor_notice_key ) ) { set_transient( $cursor_notice_key, 1, HOUR_IN_SECONDS ); $logger = new \WC_Logger(); $logger->add( 'Product Feed Pro by AdTribes.io', sprintf( 'Batch cursor disabled for feed #%d: the "adt_product_feed_get_products_query_args" filter changed the query orderby away from ( post_date DESC, ID DESC ), so this run falls back to LIMIT/OFFSET paging.', $feed->id ), 'warning' ); } } if ( $use_cursor ) { unset( $wp_query['offset'] ); } else { unset( $wp_query[ Product_Data::BATCH_CURSOR_QUERY_ARG ] ); } $prods = new WP_Query( $wp_query ); // Increment preview query counter if ( $is_preview_mode ) { $preview_query_count++; } // Prime post + meta caches for this batch's products and their candidate // images so wp_get_attachment_metadata() inside the loop doesn't trigger an // N+1 query against attachment postmeta. if ( $image_size_validation_in_effect && ! empty( $prods->posts ) ) { \AdTribes\PFP\Helpers\Image_Size_Validator::prime_caches_for_products( $prods->posts ); } // Last row this query walked, in query order. Reset per query so an empty // result leaves the cursor where it was. $batch_last_id = 0; while ( $prods->have_posts() ) : $prods->the_post(); // Every row that is walked advances the resume point, including the ones // skipped further down by the feed filters - a skipped row must not be // fetched again by the next batch. $batch_last_id = (int) get_the_ID(); $attr_line = ''; $catname = array(); $catlink = array(); $xml_product = array(); /** * Filter the product ID that is being processed. * * @since 13.3.7.1 * * @param int $product_id The product ID. * @param Product_Feed $feed The product feed instance. * @return int The product ID. */ $product_id = apply_filters( 'adt_product_feed_get_product_id', get_the_ID(), $feed ); $product = wc_get_product( $product_id ); if ( ! is_a( $product, 'WC_Product' ) ) { continue; } $parent_id = wp_get_post_parent_id( $product_id ); $parent_product = $parent_id > 0 ? wc_get_product( $parent_id ) : null; $product_data['id'] = $product_id; // Set country code. $country_code = $feed->country ?? ''; // Only products that have been sold are allowed to go through if ( $feed->utm_total_product_orders_lookback && $feed->utm_total_product_orders_lookback > 0 ) { if ( ! in_array( $product_data['id'], $allowed_product_orders ) ) { continue; } } // Only products that are visible in the catalog are allowed to go through. $catalog_visibility = $product->get_catalog_visibility(); /** * Filter the catalog visibility. * * @since 13.4.5 * * @param array $catalog_visibility The catalog visibility. * @param Product_Feed $feed The product feed instance. * @return array The catalog visibility. */ if ( in_array( $catalog_visibility, apply_filters( 'adt_product_feed_filter_catalog_visibility', array(), $feed ) ) ) { continue; } // Only products that are visible in the catalog are allowed to go through. $catalog_visibility = $product->get_catalog_visibility(); /** * Filter the catalog visibility. * * @since 13.4.5 * * @param array $catalog_visibility The catalog visibility. * @param Product_Feed $feed The product feed instance. * @return array The catalog visibility. */ if ( in_array( $catalog_visibility, apply_filters( 'adt_product_feed_filter_catalog_visibility', array(), $feed ) ) ) { continue; } $product_data['title'] = Sanitization::sanitize_html_content( $product->get_title(), $feed ); $product_data['mother_title'] = $product_data['title']; $product_data['raw_title'] = html_entity_decode( Sanitization::sanitize_raw_html_content( $product->get_title() ), ENT_QUOTES | ENT_XML1, 'UTF-8' ); $product_data['raw_mother_title'] = $product_data['raw_title']; $product_data['title_hyphen'] = $product_data['title']; $product_data['title_slug'] = $product->get_slug(); $product_data['sku'] = $product->get_sku(); $product_data['sku_id'] = $product_data['id']; $product_data['wc_post_id_product_id'] = 'wc_post_id_' . $product_data['id']; $product_data['publication_date'] = date( 'F j, Y, G:i a' ); $product_data['add_to_cart_link'] = trailingslashit( wc_get_page_permalink( 'shop' ) ) . '?add-to-cart=' . $product_data['id']; $product_data['cart_link'] = trailingslashit( wc_get_cart_url() ) . '?add-to-cart=' . $product_data['id']; $product_data['visibility'] = $catalog_visibility; $product_data['boolean_true'] = 'true'; $product_data['boolean_false'] = 'false'; // WooCommerce base country (used by e.g. the OpenAI feed target_countries/store_country fields). $product_data['base_country'] = WC()->countries->get_base_country(); // Site URL $product_data['site_url'] = get_site_url(); // Site title $product_data['site_title'] = get_bloginfo( 'name' ); // WooCommerce terms and conditions page URL $wc_terms_page_id = wc_get_page_id( 'terms' ); $product_data['terms_condtion_page_url'] = $wc_terms_page_id > 0 ? get_permalink( $wc_terms_page_id ) : ''; // WP Privacy Policy page URL $wp_privacy_policy_page_id = get_option( 'wp_page_for_privacy_policy' ); $product_data['privacy_policy_page_url'] = $wp_privacy_policy_page_id > 0 ? get_permalink( $wp_privacy_policy_page_id ) : ''; // WooCommerce shop page URL $wc_shop_page_id = wc_get_page_id( 'shop' ); $product_data['shop_url'] = $wc_shop_page_id > 0 ? get_permalink( $wc_shop_page_id ) : ''; // Get product creation date // For variations, use the parent's creation date to maintain consistency with variable product creation date changes $creation_date_source = $product; if ( $product->get_type() === 'variation' && $parent_id > 0 && is_object( $parent_product ) ) { $creation_date_source = $parent_product; } if ( ! empty( $creation_date_source->get_date_created() ) ) { $datetime_created = $creation_date_source->get_date_created(); $timestamp_created = $datetime_created->getTimestamp(); $timestamp_now = ( new WC_DateTime() )->getTimestamp(); $time_delta = $timestamp_now - $timestamp_created; $product_data['days_back_created'] = round( $time_delta / 86400 ); $product_data['product_creation_date'] = $datetime_created; } // Start product visibility default value. $product_data['exclude_from_catalog'] = 'no'; $product_data['exclude_from_search'] = 'no'; $product_data['exclude_from_all'] = 'no'; // For product variations, check the parent product's featured status if ( $parent_id > 0 && is_object( $parent_product ) ) { $product_data['featured'] = $parent_product->is_featured() ? 'yes' : 'no'; } else { $product_data['featured'] = $product->is_featured() ? 'yes' : 'no'; } switch ( $catalog_visibility ) { case 'catalog': $product_data['exclude_from_search'] = 'yes'; break; case 'search': $product_data['exclude_from_catalog'] = 'yes'; break; case 'hidden': $product_data['exclude_from_catalog'] = 'yes'; $product_data['exclude_from_search'] = 'yes'; $product_data['exclude_from_all'] = 'yes'; break; case 'visible': default: break; } // Get product tax details $product_data['tax_status'] = $product->get_tax_status(); $product_data['tax_class'] = $product->get_tax_class(); $product_data['tax_class_name'] = isset( $tax_class_options[ $product_data['tax_class'] ] ) ? $tax_class_options[ $product_data['tax_class'] ] : ''; // End product visibility logic $product_data['item_group_id'] = $parent_id ?? ''; if ( ! empty( $product_data['sku'] ) ) { $product_data['sku_id'] = $product_data['sku'] . '_' . $product_data['id']; if ( $feed_channel['fields'] == 'facebook_drm' ) { if ( $product_data['item_group_id'] > 0 ) { $product_data['sku_item_group_id'] = $product_data['sku'] . '_' . $product_data['item_group_id']; } else { $product_data['sku_item_group_id'] = $product_data['sku'] . '_' . $product_data['id']; } } } $cat_alt = array(); $cat_term = ''; $cat_order = ''; $categories = array(); if ( $parent_id && is_object( $parent_product ) && is_a( $parent_product, 'WC_Product_Variable' ) && method_exists( $parent_product, 'get_category_ids' ) ) { $categories = $parent_product->get_category_ids(); } elseif ( is_object( $product ) && method_exists( $product, 'get_category_ids' ) ) { $categories = $product->get_category_ids(); } $cat_alt = $categories; // Get number of sales for this product // Variations require direct order item queries as WooCommerce doesn't track variation sales in post meta. if ( $is_total_product_orders_mapped ) { if ( $product->is_type( 'variation' ) ) { $product_data['total_product_orders'] = \AdTribes\PFP\Classes\Orders::get_variation_total_sales( $product->get_id() ); } else { $product_data['total_product_orders'] = $product->get_total_sales(); } } // Determine real category hierarchy $cat_order = array(); foreach ( $categories as $key => $value ) { $product_cat = get_term( $value, 'product_cat' ); // Not in array so we can add it if ( ! in_array( $value, $cat_order ) ) { $parent_cat = $product_cat->parent; // Check if parent is in array if ( in_array( $parent_cat, $cat_order ) ) { // Parent is in array, now determine position $position = array_search( $parent_cat, $cat_order ); // Use array splice to add it in the right position in array $new_position = $position + 1; // Insert on new position in array array_splice( $cat_order, $new_position, 0, $value ); } else { // Parent is not in array if ( $parent_cat > 0 ) { if ( in_array( $parent_cat, $categories ) ) { $cat_order[] = $parent_cat; } $cat_order[] = $value; } else { // This is the MAIN cat so should be in front array_unshift( $cat_order, $value ); } } } } $categories = $cat_order; // This is a category fix for Yandex, probably needed for all channels // When Yoast is not installed and a product is linked to multiple categories // The ancestor categoryId does not need to be in the feed $double_categories = array( 0 => 'Yandex', 1 => 'Prisjakt.se', 2 => 'Pricerunner.se', 3 => 'Pricerunner.dk', ); if ( in_array( $feed_channel['name'], $double_categories, true ) ) { $cat_alt = array(); if ( $product_data['item_group_id'] > 0 ) { $cat_terms = get_the_terms( $product_data['item_group_id'], 'product_cat' ); } else { $cat_terms = get_the_terms( $product_data['id'], 'product_cat' ); } if ( $cat_terms ) { foreach ( $cat_terms as $cat_term ) { $cat_alt[] = $cat_term->term_id; } } $categories = $cat_alt; } $product_data['category_path'] = ''; // Sort categories so the category with the highest category ID being used for the category path attributes asort( $categories ); foreach ( $categories as $key => $value ) { $product_cat = get_term( (int) $value, 'product_cat' ); if ( $product_cat && ! is_wp_error( $product_cat ) ) { // Check if there are mother categories $category_path = $this->woosea_get_term_parents( $product_cat->term_id, 'product_cat', $link = false, $project_taxonomy = $feed_channel['taxonomy'], $nicename = false, $visited = array() ); if ( ! is_object( $category_path ) ) { $category_path_skroutz = preg_replace( '/>/', '>', $category_path ); $product_data['category_path'] = $category_path; $product_data['category_path_skroutz'] = $category_path_skroutz; $product_data['category_path_skroutz'] = str_replace( 'Home >', '', $product_data['category_path_skroutz'] ); $product_data['category_path_skroutz'] = str_replace( '&', '&', $product_data['category_path_skroutz'] ); } $parent_categories = get_ancestors( (int) $product_cat->term_id, 'product_cat' ); if ( is_array( $parent_categories ) && ! empty( $parent_categories ) ) { foreach ( $parent_categories as $category_id ) { $parent = get_term_by( 'id', (int) $category_id, 'product_cat' ); if ( $parent && ! is_wp_error( $parent ) ) { $parent_name = $parent->name; } } } if ( isset( $product_cat->name ) ) { $catname[] = $product_cat->name; $term_link = get_term_link( (int) $value, 'product_cat' ); if ( ! is_wp_error( $term_link ) ) { $catlink[] = $term_link; } } } } // Get the Yoast primary category (if exists) if ( class_exists( 'WPSEO_Primary_Term' ) ) { // Show the post's 'Primary' category, if this Yoast feature is available, & one is set $item_id = $product_data['id']; if ( $product_data['item_group_id'] > 0 ) { $item_id = $product_data['item_group_id']; } $wpseo_primary_term = new WPSEO_Primary_Term( 'product_cat', $item_id ); $prm_term = $wpseo_primary_term->get_primary_term(); $prm_cat = get_term( $prm_term, 'product_cat' ); if ( ! is_wp_error( $prm_cat ) ) { if ( ! empty( $prm_cat->name ) ) { $product_data['category_path'] = $this->woosea_get_term_parents( $prm_cat->term_id, 'product_cat', $link = false, $project_taxonomy = $feed_channel['taxonomy'], $nicename = false, $visited = array() ); $product_data['one_category'] = $prm_cat->name; } } unset( $prm_cat ); unset( $prm_term ); unset( $wpseo_primary_term ); } // Get the RankMath primary category if ( Helper::is_plugin_active( 'seo-by-rank-math/rank-math.php' ) ) { $item_id = $product_data['id']; if ( $product_data['item_group_id'] > 0 ) { $item_id = $product_data['item_group_id']; } $primary_cat_id = get_post_meta( $item_id, 'rank_math_primary_product_cat', true ); if ( $primary_cat_id ) { $product_cat = get_term( $primary_cat_id, 'product_cat' ); if ( ! empty( $product_cat->name ) ) { $product_data['category_path'] = $this->woosea_get_term_parents( $product_cat->term_id, 'product_cat', $link = false, $project_taxonomy = $feed_channel['taxonomy'], $nicename = false, $visited = array() ); $product_data['one_category'] = $product_cat->name; } } unset( $primary_cat_id ); } $product_data['category_path_short'] = str_replace( 'Home >', '', $product_data['category_path'] ); $product_data['category_path_short'] = str_replace( '>', '>', $product_data['category_path_short'] ); $product_data['category_link'] = implode( '||', $catlink ); $product_data['raw_categories'] = implode( '||', $catname ); $product_data['categories'] = implode( '||', $catname ); $product_data['categories_comma_separated'] = $this->get_comma_separated_categories( $product->get_category_ids() ); // Product Description. $product_description = $product->get_description(); $product_short_description = $product->get_short_description(); $parent_product_description = is_object( $parent_product ) && method_exists( $parent_product, 'get_description' ) ? $parent_product->get_description() : $product_description; $parent_product_short_description = is_object( $parent_product ) && method_exists( $parent_product, 'get_short_description' ) ? $parent_product->get_short_description() : $product_short_description; $combined_description = $product_description; $combined_short_description = $product_short_description; if ( 'variation' === $product->get_type() ) { $combined_description = '' !== $parent_product_description ? $parent_product_description . ' ' . $product_description : $product_description; $combined_short_description = '' !== $parent_product_short_description ? $parent_product_short_description . ' ' . $product_short_description : $product_short_description; } // Raw descriptions, unfiltered $product_data['raw_description'] = Sanitization::sanitize_raw_html_content( $combined_description ); $product_data['raw_short_description'] = Sanitization::sanitize_raw_html_content( $combined_short_description ); $product_data['raw_parent_description'] = Sanitization::sanitize_raw_html_content( $parent_product_description ); $product_data['raw_parent_short_description'] = Sanitization::sanitize_raw_html_content( $parent_product_short_description ); $product_data['raw_variation_description'] = Sanitization::sanitize_raw_html_content( $product_description ); $product_data['raw_variation_short_description'] = Sanitization::sanitize_raw_html_content( $product_short_description ); // Sanitize descriptions $product_data['description'] = Sanitization::sanitize_html_content( $combined_description, $feed ); $product_data['short_description'] = Sanitization::sanitize_html_content( $combined_short_description, $feed ); $product_data['mother_description'] = Sanitization::sanitize_html_content( $parent_product_description, $feed ); $product_data['mother_short_description'] = Sanitization::sanitize_html_content( $parent_product_short_description, $feed ); $product_data['variation_description'] = Sanitization::sanitize_html_content( $product_description, $feed ); $product_data['variation_short_description'] = Sanitization::sanitize_html_content( $product_short_description, $feed ); // Truncate description on 5000 characters for Google Shopping if ( $feed_channel['fields'] == 'google_shopping' ) { $product_data['description'] = mb_substr( $product_data['description'], 0, 5000 ); $product_data['short_description'] = mb_substr( $product_data['short_description'], 0, 5000 ); $product_data['mother_description'] = mb_substr( $product_data['mother_description'], 0, 5000 ); $product_data['mother_short_description'] = mb_substr( $product_data['mother_short_description'], 0, 5000 ); $product_data['variation_description'] = mb_substr( $product_data['variation_description'], 0, 5000 ); $product_data['variation_short_description'] = mb_substr( $product_data['variation_short_description'], 0, 5000 ); $product_data['raw_description'] = mb_substr( $product_data['raw_description'], 0, 5000 ); $product_data['raw_short_description'] = mb_substr( $product_data['raw_short_description'], 0, 5000 ); $product_data['raw_parent_description'] = mb_substr( $product_data['raw_parent_description'], 0, 5000 ); $product_data['raw_parent_short_description'] = mb_substr( $product_data['raw_parent_short_description'], 0, 5000 ); $product_data['raw_variation_description'] = mb_substr( $product_data['raw_variation_description'], 0, 5000 ); $product_data['raw_variation_short_description'] = mb_substr( $product_data['raw_variation_short_description'], 0, 5000 ); } /** * Check of we need to add Google Analytics UTM parameters */ if ( $feed->utm_enabled ) { $utm_part = $this->woosea_append_utm_code( $feed, get_the_ID(), $parent_id, get_permalink( $product_data['id'] ) ); } else { $utm_part = ''; } $product_data['link'] = get_permalink( $product_data['id'] ) . "$utm_part"; $product_data['link_no_tracking'] = get_permalink( $product_data['id'] ); $variable_link = htmlspecialchars( get_permalink( $product_data['id'] ) ); $vlink_piece = explode( '?', $variable_link ); $qutm_part = ltrim( $utm_part, '&' ); $qutm_part = ltrim( $qutm_part, 'amp;' ); $qutm_part = ltrim( $qutm_part, '?' ); if ( $qutm_part ) { $product_data['variable_link'] = $vlink_piece[0] . '?' . $qutm_part; $product_data['link_no_tracking'] = $vlink_piece[0]; } else { $product_data['variable_link'] = $vlink_piece[0]; $product_data['link_no_tracking'] = $vlink_piece[0]; } // For product variations, check the parent product's purchase note if ( $parent_id > 0 && is_object( $parent_product ) ) { $product_data['purchase_note'] = $parent_product->get_purchase_note(); } else { $product_data['purchase_note'] = $product->get_purchase_note(); } // Product condition is default to 'new' if not set. if ( empty( $product_data['condition'] ) || $product_data['condition'] == 'Array' ) { $product_data['condition'] = 'new'; } // get_stock only works as of WC 5 and higher? $product_data['availability'] = $this->get_stock( $product_id ); /** * When 'Enable stock management at product level is active * availability will always return out of stock, even when the stock quantity > 0 * Therefor, we need to check the stock_status and overwrite te availability value */ if ( ! is_bool( $product ) ) { $stock_status = $product->get_stock_status(); } else { $stock_status = 'instock'; } $product_data['stock_status'] = $stock_status; if ( 'outofstock' === $stock_status ) { $product_data['availability'] = 'out of stock'; if ( ( 'google_shopping' === $feed_channel['taxonomy'] ) && ( 'google_shopping' === $feed_channel['fields'] ) ) { $product_data['availability'] = 'out_of_stock'; if ( 'Twitter' === $feed_channel['name'] ) { $product_data['availability'] = 'out of stock'; } } elseif ( ( 'google_shopping' === $feed_channel['taxonomy'] ) && ( 'google_local' === $feed_channel['fields'] ) ) { $product_data['availability'] = 'out_of_stock'; } if ( preg_match( '/fruugo/i', $feed_channel['fields'] ) ) { $product_data['availability'] = 'OUTOFSTOCK'; } } elseif ( $stock_status == 'onbackorder' ) { $product_data['availability'] = 'on backorder'; if ( ( 'google_shopping' === $feed_channel['taxonomy'] ) && ( 'google_shopping' === $feed_channel['fields'] ) ) { $product_data['availability'] = 'backorder'; if ( 'Twitter' === $feed_channel['name'] ) { $product_data['availability'] = 'available for order'; } } elseif ( ( 'google_shopping' === $feed_channel['taxonomy'] ) && ( 'google_local' === $feed_channel['fields'] ) ) { $product_data['availability'] = 'on_display_to_order'; } if ( preg_match( '/fruugo/i', $feed_channel['fields'] ) ) { $product_data['availability'] = 'OUTOFSTOCK'; } } else { $product_data['availability'] = 'in stock'; if ( ( 'google_shopping' === $feed_channel['taxonomy'] ) && ( 'google_shopping' === $feed_channel['fields'] ) ) { $product_data['availability'] = 'in_stock'; if ( 'Twitter' === $feed_channel['name'] ) { $product_data['availability'] = 'in stock'; } } elseif ( ( 'google_shopping' === $feed_channel['taxonomy'] ) && ( 'google_local' === $feed_channel['fields'] ) ) { $product_data['availability'] = 'in_stock'; } if ( preg_match( '/fruugo/i', $feed_channel['fields'] ) ) { $product_data['availability'] = 'INSTOCK'; } } /** * Filter the availability format. * * @since 13.4.9 * * @param string $availability The availability value. * @param object $product The product object. * @param array $feed_channel The feed channel array. * @param object $feed The feed object. * @return string The availability value. */ $product_data['availability'] = apply_filters( 'adt_product_data_availability_format', $product_data['availability'], $product, $feed_channel, $feed ); // Create future availability dates if ( $product->is_on_backorder() ) { $now = new WC_DateTime( 'now', new DateTimeZone( 'UTC' ) ); // Set local timezone or offset. if ( get_option( 'timezone_string' ) ) { $now->setTimezone( new DateTimeZone( wc_timezone_string() ) ); } else { $now->set_utc_offset( wc_timezone_offset() ); } $now->setTime(0, 0); $plus_week_to = 8; $date = new WC_DateTime( $now ); for ($i = 1; $i <= $plus_week_to; $i++) { $date_plus_week = clone $date; $date_plus_week->modify("+$i week"); $product_data["availability_date_plus{$i}week"] = $date_plus_week->__toString(); } } $product_data['author'] = get_the_author(); $product_data['quantity'] = $product->get_stock_quantity(); $download = $product->is_downloadable(); if ( $download == 1 ) { $product_data['downloadable'] = 'yes'; } else { $product_data['downloadable'] = 'no'; } unset( $download ); $virtual = $product->is_virtual(); if ( $virtual == 1 ) { $product_data['virtual'] = 'yes'; } else { $product_data['virtual'] = 'no'; } unset( $virtual ); $product_data['menu_order'] = get_post_field( 'menu_order', $product_data['id'] ); $product_data['currency'] = Product_Feed_Helper::get_product_data_currency(); if ( $product->is_on_sale() ) { $sales_price_date_from = Formatting::format_date( $product->get_date_on_sale_from(), $feed ); $sales_price_date_to = Formatting::format_date( $product->get_date_on_sale_to(), $feed ); $product_data['sale_price_effective_date'] = $sales_price_date_from . '/' . $sales_price_date_to; } else { $product_data['sale_price_effective_date'] = ''; } $product_data['sale_price_start_date'] = $product->get_date_on_sale_from() ? Formatting::format_date( $product->get_date_on_sale_from(), $feed ) : ''; $product_data['sale_price_end_date'] = $product->get_date_on_sale_to() ? Formatting::format_date( $product->get_date_on_sale_to(), $feed ) : ''; $product_image = wp_get_attachment_url( $product->get_image_id() ); $product_data['image'] = $product_image ? $product_image : ''; $non_local_image = wp_get_attachment_image_src( get_post_thumbnail_id( $product_data['id'] ), 'single-post-thumbnail' ); if ( is_array( $non_local_image ) ) { $product_data['non_local_image'] = $non_local_image[0]; } unset( $non_local_image ); $product_data['image_all'] = $product_data['image']; $product_data['all_images'] = $product_data['image']; $product_data['all_gallery_images'] = ''; $product_data['product_type'] = $product->get_type(); // Get the number of active variations that are on stock for variable products if ( ( $product_data['item_group_id'] > 0 ) && ( $product_data['product_type'] == 'variation' ) ) { $parent_product = wc_get_product( $product_data['item_group_id'] ); if ( is_object( $parent_product ) ) { $current_products = $parent_product->get_children(); $product_data['nr_variations'] = count( $current_products ); $vcnt = 0; foreach ( $current_products as $ckey => $cvalue ) { $stock_value = get_post_meta( $cvalue, '_stock_status', true ); if ( $stock_value == 'instock' ) { ++$vcnt; } } // unset($current_products); $product_data['nr_variations_stock'] = $vcnt; } else { $product_data['nr_variations'] = 9999; $product_data['nr_variations_stock'] = 9999; } // unset($parent_product); } else { $product_data['nr_variations'] = 9999; $product_data['nr_variations_stock'] = 9999; } // For variable products I need to get the product gallery images of the simple mother product if ( $product_data['item_group_id'] > 0 ) { $parent_product = wc_get_product( $product_data['item_group_id'] ); if ( is_object( $parent_product ) ) { $gallery_ids = $parent_product->get_gallery_image_ids(); $product_data['image_all'] = wp_get_attachment_url( $parent_product->get_image_id() ); $gal_id = 1; foreach ( $gallery_ids as $gallery_key => $gallery_value ) { $product_data[ 'image_' . $gal_id ] = wp_get_attachment_url( $gallery_value ); $product_data['all_images'] .= ',' . wp_get_attachment_url( $gallery_value ); $product_data['all_gallery_images'] .= ',' . wp_get_attachment_url( $gallery_value ); ++$gal_id; } } // unset($parent_product); } else { $gallery_ids = $product->get_gallery_image_ids(); $gal_id = 1; foreach ( $gallery_ids as $gallery_key => $gallery_value ) { $product_data[ 'image_' . $gal_id ] = wp_get_attachment_url( $gallery_value ); $product_data['all_images'] .= ',' . wp_get_attachment_url( $gallery_value ); $product_data['all_gallery_images'] .= ',' . wp_get_attachment_url( $gallery_value ); ++$gal_id; } // unset($gallery_ids); } $product_data['all_images'] = ltrim( $product_data['all_images'], ',' ); $product_data['all_images_kogan'] = preg_replace( '/,/', '|', $product_data['all_images'] ); $product_data['all_gallery_images'] = ltrim( $product_data['all_gallery_images'], ',' ); // Google Merchant Center and Facebook/Meta Catalog reject product images below 500x500 px. // Swap the primary to the first qualifying gallery image, or skip the product if none qualifies. // The validated URL is held in a local variable so it can be reused by the feature_image // and parent-override blocks below without polluting $product_data with scratch state. $validated_image_url = ''; if ( $image_size_validation_in_effect ) { $primary_image_id = (int) $product->get_image_id(); $is_variation = $product_data['item_group_id'] > 0 && is_object( $parent_product ); $candidate_image_ids = array( $primary_image_id ); // For variations, also consider the parent product's primary + gallery images, // since `image_all` and the variation's fallback both resolve through the parent. if ( $is_variation ) { $candidate_image_ids[] = (int) $parent_product->get_image_id(); $candidate_image_ids = array_merge( $candidate_image_ids, array_map( 'intval', (array) $parent_product->get_gallery_image_ids() ) ); } else { $candidate_image_ids = array_merge( $candidate_image_ids, array_map( 'intval', (array) $product->get_gallery_image_ids() ) ); } // Drop zero IDs before validating. If the product has NO real attachments at // all (primary and gallery both empty), preserve the pre-existing behaviour of // letting the row through with whatever image URL was resolved earlier — image // size enforcement targets undersized images, not the orthogonal "no image" // case which the channel will reject on its own. $real_candidate_image_ids = array_values( array_unique( array_filter( $candidate_image_ids ) ) ); $qualifying_image_id = ! empty( $real_candidate_image_ids ) ? \AdTribes\PFP\Helpers\Image_Size_Validator::find_qualifying_attachment( $real_candidate_image_ids, $image_size_min_dimensions['width'], $image_size_min_dimensions['height'] ) : null; if ( ! empty( $real_candidate_image_ids ) && null === $qualifying_image_id ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( sprintf( '[adt-pfp] Excluded product %d from %s feed: no image meets the minimum %dx%d size.', (int) $product_data['id'], (string) $feed_channel['fields'], $image_size_min_dimensions['width'], $image_size_min_dimensions['height'] ) ); } unset( $product_data, $xml_product ); continue; } if ( null !== $qualifying_image_id && $qualifying_image_id !== $primary_image_id ) { $qualifying_url = wp_get_attachment_url( $qualifying_image_id ); $product_data['image'] = $qualifying_url ? $qualifying_url : $product_data['image']; $product_data['image_all'] = $product_data['image']; if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( sprintf( '[adt-pfp] Swapped undersized primary image for product %d in %s feed; using attachment %d.', (int) $product_data['id'], (string) $feed_channel['fields'], $qualifying_image_id ) ); } } // Record the validated URL so the feature_image and parent-override blocks below // can fall back to it if their own image source is undersized. $validated_image_url = $product_data['image']; } $product_data['content_type'] = 'product'; if ( $product_data['product_type'] == 'variation' ) { $product_data['content_type'] = 'product_group'; } $rating_count = $product->get_rating_count(); if ( $rating_count > 1 ) { $product_data['rating_total'] = $rating_count; $product_data['rating_average'] = $product->get_average_rating(); } // When a product has no reviews than remove the 0 rating if ( isset( $product_data['rating_average'] ) && $product_data['rating_average'] == 0 ) { unset( $product_data['rating_average'] ); } $product_data['shipping'] = array(); $shipping_class_id = $product->get_shipping_class_id(); $class_cost_id = 'class_cost_' . $shipping_class_id; if ( $class_cost_id == 'class_cost_0' ) { $class_cost_id = 'no_class_cost'; } $product_data['shipping_label'] = $product->get_shipping_class(); $term = get_term_by( 'slug', $product->get_shipping_class(), 'product_shipping_class' ); if ( is_object( $term ) ) { $product_data['shipping_label_name'] = $term->name; } $base_tax_rates = \WC_Tax::get_base_tax_rates( $product->get_tax_class() ); $tax_rates = Product_Feed_Helper::find_tax_rates( array( 'country' => $feed->country ?? '', 'state' => '', 'postcode' => '', 'city' => '', 'tax_class' => $product->get_tax_class(), ), $feed, $product ); // Get product prices $product_data['price'] = Product_Feed_Helper::get_price_including_tax( $product->get_price(), $tax_rates, $feed, $product ); $product_data['regular_price'] = Product_Feed_Helper::get_price_including_tax( $product->get_regular_price(), $tax_rates, $feed, $product ); $product_data['system_price'] = Product_Feed_Helper::get_price_including_tax( $product->get_price(), $base_tax_rates, $feed, $product ); $product_data['system_regular_price'] = Product_Feed_Helper::get_price_including_tax( $product->get_regular_price(), $base_tax_rates, $feed, $product ); // Determine the gross prices of products. // The 'price_forced' is the price including tax. $product_data['price_forced'] = Product_Feed_Helper::get_price_including_tax( $product->get_price(), $tax_rates, $feed, $product ); $product_data['regular_price_forced'] = Product_Feed_Helper::get_price_including_tax( $product->get_regular_price(), $tax_rates, $feed, $product ); $product_data['system_price_forced'] = Product_Feed_Helper::get_price_including_tax( $product->get_price(), $base_tax_rates, $feed, $product ); $product_data['system_regular_price_forced'] = Product_Feed_Helper::get_price_including_tax( $product->get_regular_price(), $base_tax_rates, $feed, $product ); // The 'net_price' is the price excluding tax. $product_data['net_price'] = Product_Feed_Helper::get_price_excluding_tax( $product->get_price(), $tax_rates, $feed, $product ); $product_data['net_regular_price'] = Product_Feed_Helper::get_price_excluding_tax( $product->get_regular_price(), $tax_rates, $feed, $product ); $product_data['system_net_price'] = Product_Feed_Helper::get_price_excluding_tax( $product->get_price(), $base_tax_rates, $feed, $product ); $product_data['system_net_regular_price'] = Product_Feed_Helper::get_price_excluding_tax( $product->get_regular_price(), $base_tax_rates, $feed, $product ); if ( $product->is_on_sale() ) { $product_data['sale_price'] = Product_Feed_Helper::get_price_including_tax( $product->get_sale_price(), $tax_rates, $feed, $product ); $product_data['sale_price_forced'] = Product_Feed_Helper::get_price_including_tax( $product->get_sale_price(), $tax_rates, $feed, $product ); $product_data['net_sale_price'] = Product_Feed_Helper::get_price_excluding_tax( $product->get_sale_price(), $tax_rates, $feed, $product ); $product_data['system_sale_price'] = Product_Feed_Helper::get_price_including_tax( $product->get_sale_price(), $base_tax_rates, $feed, $product ); $product_data['system_sale_price_forced'] = Product_Feed_Helper::get_price_excluding_tax( $product->get_sale_price(), $base_tax_rates, $feed, $product ); $product_data['system_net_sale_price'] = Product_Feed_Helper::get_price_including_tax( $product->get_sale_price(), $base_tax_rates, $feed, $product ); } // WooCommerce Cost of Goods Sold if ( wc_get_container()->get(Automattic\WooCommerce\Internal\Features\FeaturesController::class)->feature_is_enabled('cost_of_goods_sold') ) { $cogs_effective_value = $product->get_cogs_effective_value(); $cogs_value = $product->get_cogs_value(); $cogs_total_value = $product->get_cogs_total_value(); // For cost_of_goods_sold, use effective value, but for variations that inherit, use total value if ( $product->is_type( 'variation' ) && is_null( $cogs_value ) && $cogs_total_value > 0 ) { $product_data['cost_of_goods_sold'] = $cogs_total_value; } else { $product_data['cost_of_goods_sold'] = $cogs_effective_value > 0 ? $cogs_effective_value : ''; } // For inherited values, use the total value instead of null $product_data['cost_of_goods_value'] = ! is_null( $cogs_value ) && $cogs_value > 0 ? $cogs_value : ($cogs_total_value > 0 ? $cogs_total_value : ''); $product_data['cost_of_goods_total_value'] = $cogs_total_value > 0 ? $cogs_total_value : ''; } $args = array( 'ex_tax_label' => false, 'currency' => '', 'decimal_separator' => wc_get_price_decimal_separator(), 'thousand_separator' => wc_get_price_thousand_separator(), 'decimals' => wc_get_price_decimals(), 'price_format' => get_woocommerce_price_format(), ); if ( isset( $product_data['price'] ) ) { $dec_price = wc_price( $product_data['price'], $args ); preg_match( '/(.*?) /', $dec_price, $matches ); if ( isset( $matches[1] ) ) { $product_data['separator_price'] = $matches[1]; } // unset($dec_price); } if ( isset( $product_data['regular_price'] ) ) { $dec_regular_price = wc_price( $product_data['regular_price'], $args ); preg_match( '/(.*?) /', $dec_regular_price, $matches_reg ); if ( isset( $matches_reg[1] ) ) { $product_data['separator_regular_price'] = $matches_reg[1]; } } if ( isset( $product_data['sale_price'] ) ) { $dec_sale_price = wc_price( $product_data['sale_price'], $args ); preg_match( '/(.*?) /', $dec_sale_price, $matches_sale ); if ( isset( $matches_sale[1] ) ) { $product_data['separator_sale_price'] = $matches_sale[1]; } } $all_standard_taxes = WC_Tax::get_rates_for_tax_class( '' ); $nr_standard_rates = count( $all_standard_taxes ); if ( ! empty( $all_standard_taxes ) && ( $nr_standard_rates > 1 ) ) { foreach ( $all_standard_taxes as $rate ) { $rate_arr = get_object_vars( $rate ); if ( $rate_arr['tax_rate_country'] == $country_code ) { $tax_rates[1]['rate'] = $rate_arr['tax_rate']; } unset( $rate_arr ); } } elseif ( ! empty( $tax_rates ) ) { foreach ( $tax_rates as $tk => $tv ) { if ( $tv['rate'] > 0 ) { $tax_rates[1]['rate'] = $tv['rate']; } else { $tax_rates[1]['rate'] = 0; } } } else { $tax_rates[1]['rate'] = 0; } if ( empty( $tax_rates[1]['rate'] ) ) { if ( ! empty( $all_standard_taxes ) && ( $nr_standard_rates > 1 ) ) { foreach ( $all_standard_taxes as $rate ) { $rate_arr = get_object_vars( $rate ); if ( $rate_arr['tax_rate_country'] == '' ) { $tax_rates[1]['rate'] = $rate_arr['tax_rate']; } unset( $rate_arr ); } } } // unset($all_standard_taxes); $tax_rates_first = reset( $tax_rates ); $fullrate = ! empty( $tax_rates_first ) ? 100 + $tax_rates_first['rate'] : 100; if ( array_key_exists( 1, $tax_rates ) ) { $product_data['vat'] = $tax_rates[1]['rate']; } // Override price when bundled or composite product if ( ( $product->get_type() == 'bundle' ) || ( $product->get_type() == 'composite' ) ) { $meta = get_post_meta( $product_data['id'] ); if ( $product->get_type() == 'bundle' ) { if ( Helper::is_plugin_active( 'woocommerce-product-bundles/woocommerce-product-bundles.php' ) ) { if ( ! empty( $product->get_bundle_price() ) ) { $product_data['price'] = $product->get_bundle_price_including_tax(); $product_data['price_forced'] = $product->get_bundle_price_including_tax(); $product_data['regular_price'] = $product->get_bundle_regular_price(); $product_data['regular_price_forced'] = $product->get_bundle_regular_price_including_tax(); if ( $product_data['price'] != $product_data['regular_price'] ) { $product_data['sale_price'] = $product->get_bundle_price(); $product_data['sale_price_forced'] = $product->get_bundle_price_including_tax(); } // Unset sale price when it is 0.00 if ( isset( $product_data['sale_price'] ) && $product_data['sale_price'] == '0.00' ) { unset( $product_data['sale_price'] ); } } } } else { // Composite product if ( Helper::is_plugin_active( 'woocommerce-composite-products/woocommerce-composite-products.php' ) ) { if ( ! empty( $product->get_composite_price() ) ) { $product_data['price'] = $product->get_composite_price_including_tax(); $product_data['price_forced'] = $product->get_composite_price_including_tax(); $product_data['regular_price'] = $product->get_composite_regular_price(); $product_data['regular_price_forced'] = $product->get_composite_regular_price_including_tax(); if ( $product_data['price'] != $product_data['regular_price'] ) { $product_data['sale_price'] = $product->get_composite_price(); $product_data['sale_price_forced'] = $product->get_composite_price_including_tax(); } // Unset sale price when it is 0.00 if ( isset( $product_data['sale_price'] ) && $product_data['sale_price'] == '0.00' ) { unset( $product_data['sale_price'] ); } } } } } // Is the Discount Rules for WooCommerce by FlyCart plugin active, check for sale prices if ( Helper::is_plugin_active( 'woo-discount-rules/woo-discount-rules.php' ) ) { $discount = apply_filters( 'advanced_woo_discount_rules_get_product_discount_price_from_custom_price', false, $product, 1, $product_data['sale_price'] ?? 0, 'discounted_price', true, true ); if ( $discount !== false ) { // round discounted price on proper decimals $decimals = wc_get_price_decimals(); if ( $decimals < 1 ) { $discount = round( $discount, 0 ); $product_data['sale_price'] = round( $discount, 0 ); $product_data['price'] = $discount; $product_data['regular_price'] = round( $product_data['regular_price'], 0 ); } else { $discount = round( $discount, 2 ); $product_data['sale_price'] = @number_format( $discount, 2 ); $product_data['price'] = $discount; } $price_incl_tax = get_option( 'woocommerce_prices_include_tax' ); if ( $price_incl_tax == 'yes' ) { $product_data['price_forced'] = $product_data['price'] * ( $fullrate / 100 ); $product_data['price_forced_rounded'] = round( $product_data['price_forced'], 0 ); $product_data['net_price'] = $product_data['price'] / ( $fullrate / 100 ); $product_data['net_price_rounded'] = round( $product_data['net_price'] ); // New Nov. 1st 2023 $product_data['net_regular_price'] = $product_data['regular_price'] / ( $fullrate / 100 ); $product_data['net_regular_price_rounded'] = round( $product_data['net_regular_price'], 0 ); // New Nov. 1st 2023 $product_data['net_sale_price'] = ( $discount / $fullrate ) * 100; // New Nov. 1st 2023 $product_data['net_sale_price_rounded'] = round( $product_data['net_sale_price'], 0 ); // New Nov. 1st 2023 $product_data['sale_price_forced'] = $discount * ( $fullrate / 100 ); $product_data['sale_price_forced_rounded'] = round( $product_data['sale_price_forced'], 0 ); } else { $product_data['net_sale_price'] = $discount; $product_data['sale_price_forced'] = round( $discount * ( $fullrate / 100 ), 2 ); $product_data['sale_price_forced_rounded'] = round( $product_data['sale_price_forced'], 0 ); } $thousand_separator = wc_get_price_thousand_separator(); if ( $thousand_separator != ',' ) { $replaceWith = ''; $product_data['price'] = preg_replace( '/,/', $replaceWith, $product_data['price'], 1 ); $product_data['regular_price'] = preg_replace( '/,/', $replaceWith, $product_data['regular_price'], 1 ); if ( isset( $product_data['sale_price'] ) && $product_data['sale_price'] > 0 ) { $product_data['sale_price'] = preg_replace( '/,/', $replaceWith, $product_data['sale_price'], 1 ); } } } // unset($discount); } // Is the Mix and Match plugin active if ( Helper::is_plugin_active( 'woocommerce-mix-and-match-products/woocommerce-mix-and-match-products.php' ) ) { if ( $product->is_type( 'mix-and-match' ) ) { if ( $product_data['price'] == '0.00' ) { $product_data['price'] = ''; $product_data['regular_price'] = ''; } // Get minimum prices $product_data['mm_min_price'] = wc_format_localized_price( $product->get_mnm_price() ); $product_data['mm_min_regular_price'] = wc_format_localized_price( $product->get_mnm_regular_price() ); // Get maximum prices $product_data['mm_max_price'] = wc_format_localized_price( $product->get_mnm_price( 'max' ) ); $product_data['mm_max_regular_price'] = wc_format_localized_price( $product->get_mnm_regular_price( 'max' ) ); } } // Calculate discount percentage if ( isset( $product_data['rounded_sale_price'] ) ) { if ( $product_data['rounded_regular_price'] > 0 ) { $disc = round( ( $product_data['rounded_sale_price'] * 100 ) / $product_data['rounded_regular_price'], 0 ); $product_data['discount_percentage'] = 100 - $disc; // $product_data['discount_percentage'] = round(100-(($product_data['sale_price']/$product_data['regular_price'])*100),2); } } // Rounded prices. $decimal_separator = wc_get_price_decimal_separator(); $number_of_decimals = apply_filters( 'adt_product_feed_data_rounded_price_number_of_decimals', 2, $feed ); $rounded_precisions = apply_filters( 'adt_product_feed_data_rounded_price_precisions', 0, $feed ); $rounded_mode = apply_filters( 'adt_product_feed_data_rounded_price_mode', PHP_ROUND_HALF_UP, $feed ); $rounded_prices = array( 'price' => 'rounded_price', 'regular_price' => 'rounded_regular_price', 'sale_price' => 'rounded_sale_price', 'price_forced' => 'price_forced_rounded', 'regular_price_forced' => 'regular_price_forced_rounded', 'sale_price_forced' => 'sale_price_forced_rounded', 'net_price' => 'net_price_rounded', 'net_regular_price' => 'net_regular_price_rounded', 'net_sale_price' => 'net_sale_price_rounded', ); foreach ( $rounded_prices as $price_key => $rounded_key ) { if ( array_key_exists( $price_key, $product_data ) && is_numeric( $product_data[ $price_key ] ) ) { $product_data[ $rounded_key ] = number_format( round( $product_data[ $price_key ], $rounded_precisions, $rounded_mode ), $number_of_decimals, $decimal_separator, '' ); } } // Check if shipping data is required based on feed attributes or channel type $shipping_required = ! empty( $feed_attributes ) && ( // Check for g:shipping attribute (Google Shopping format) ! empty( array_filter( $feed_attributes, static function ( $attr ) { return is_array( $attr ) && ( $attr['attribute'] ?? '' ) === 'g:shipping'; } ) ) && $product_data['price'] > 0 || // Check for other shipping-related attributes ! empty( array_filter( $feed_attributes, static function ( $attr ) { return is_array( $attr ) && in_array( $attr['attribute'] ?? '', [ 'shipping', 'lowest_shipping_costs', 'shipping_price' ], true ); } ) ) || // Check for specific channel types that require shipping in_array( $feed_channel['fields'], [ 'trovaprezzi', 'idealo', 'customfeed', 'zbozi', 'heureka' ], true ) ); // Fetch shipping data only once if required if ( $shipping_required ) { $shipping_data_instance = Shipping_Data::instance(); $product_data['shipping'] = $shipping_data_instance->get_shipping_data( $product, $feed ); $shipping_str = $product_data['shipping']; // Set for backward compatibility } // Initialize shipping price if shipping data exists if ( ! empty( $shipping_str ) ) { $product_data['shipping_price'] = 0; } $lowest_shipping_price = array(); $shipping_arr = $product_data['shipping']; if ( is_array( $shipping_arr ) ) { foreach ( $shipping_arr as $akey => $arr ) { // Price format is " " (e.g. "20.00 AUD"); the numeric // amount is the first segment. Heureka feeds omit the currency, so the // first segment is still the amount. $pieces_ship = explode( ' ', $arr['price'] ); if ( isset( $pieces_ship[0] ) && '' !== $pieces_ship[0] ) { $product_data['shipping_price'] = $pieces_ship[0]; $lowest_shipping_price[] = $pieces_ship[0]; } } // Check if we need to add a region foreach ( $shipping_arr as $akey => $arr ) { if ( isset( $arr['country'] ) ) { if ( preg_match( '/:/i', $arr['country'] ) ) { $region_split = explode( ':', $arr['country'] ); $sgipping_arr[ $akey ]['region'] = $region_split[1]; } } } } // Get the lowest shipping costs if ( ! empty( $lowest_shipping_price ) ) { $decimal_separator = wc_get_price_decimal_separator(); if ( $decimal_separator == ',' ) { $numeric_lowest_shipping_price = array(); foreach ( $lowest_shipping_price as &$value ) { $number = str_replace( ',', '.', $value ); if ( is_numeric( $number ) ) { $value = number_format( $number, 2, '.', '' ); $numeric_lowest_shipping_price[] = $value; } } $lowest_shipping_price = $numeric_lowest_shipping_price; // unset($value); } $nr_in = count( $lowest_shipping_price ); if ( $nr_in > 0 ) { $product_data['lowest_shipping_costs'] = min( $lowest_shipping_price ); if ( $decimal_separator == ',' ) { $product_data['lowest_shipping_costs'] = str_replace( '.', ',', $product_data['lowest_shipping_costs'] ); } } } // Google Dynamic Remarketing feeds require the English price notation if ( $feed_channel['name'] == 'Google Remarketing - DRM' ) { $thousand_separator = wc_get_price_thousand_separator(); if ( $thousand_separator != ',' ) { $product_data['price'] = floatval( str_replace( ',', '.', str_replace( '.', '', $product_data['price'] ) ) ); $product_data['regular_price'] = floatval( str_replace( ',', '.', str_replace( '.', '', $product_data['regular_price'] ) ) ); if ( isset( $product_data['sale_price'] ) && $product_data['sale_price'] > 0 ) { $product_data['sale_price'] = floatval( str_replace( ',', '.', str_replace( '.', '', $product_data['sale_price'] ) ) ); } if ( isset( $product_data['regular_price_forced'] ) ) { $product_data['regular_price_forced'] = floatval( str_replace( ',', '.', str_replace( '.', '', $product_data['regular_price_forced'] ) ) ); } if ( $product->get_sale_price() ) { $product_data['sale_price_forced'] = floatval( str_replace( ',', '.', str_replace( '.', '', $product_data['sale_price_forced'] ) ) ); } if ( $product_data['net_price'] > 0 ) { $product_data['net_price'] = floatval( str_replace( ',', '.', str_replace( '.', '', $product_data['net_price'] ) ) ); } $product_data['net_regular_price'] = @floatval( str_replace( ',', '.', str_replace( '.', '', $product_data['net_regular_price'] ) ) ); $product_data['net_sale_price'] = @floatval( str_replace( ',', '.', str_replace( '.', '', $product_data['net_sale_price'] ) ) ); $product_data['vivino_price'] = $product_data['price']; $product_data['vivino_sale_price'] = $product_data['sale_price']; $product_data['vivino_regular_price'] = $product_data['regular_price']; $product_data['vivino_net_price'] = $product_data['net_price']; $product_data['vivino_net_sale_price'] = $product_data['net_sale_price']; $product_data['vivino_net_regular_price'] = $product_data['net_regular_price']; } } // Vivino prices $product_data['vivino_price'] = floatval( str_replace( ',', '.', str_replace( ',', '.', $product_data['price'] ) ) ); $product_data['vivino_regular_price'] = floatval( str_replace( ',', '.', str_replace( ',', '.', $product_data['regular_price'] ) ) ); if ( isset( $product_data['sale_price'] ) && $product_data['sale_price'] > 0 ) { $product_data['vivino_sale_price'] = floatval( str_replace( ',', '.', str_replace( ',', '.', $product_data['sale_price'] ) ) ); if ( isset( $product_data['net_sale_price'] ) ) { $product_data['vivino_net_sale_price'] = floatval( str_replace( ',', '.', str_replace( ',', '.', $product_data['net_sale_price'] ) ) ); } } $product_data['vivino_net_price'] = floatval( str_replace( ',', '.', str_replace( ',', '.', $product_data['net_price'] ) ) ); if ( isset( $product_data['net_regular_price'] ) ) { $product_data['vivino_net_regular_price'] = floatval( str_replace( ',', '.', str_replace( ',', '.', $product_data['net_regular_price'] ) ) ); } $product_data['installment'] = $this->woosea_get_installment( $feed, $product_data['id'] ); $product_data['weight'] = ( $product->get_weight() ) ? $product->get_weight() : ''; $product_data['height'] = ( $product->get_height() ) ? $product->get_height() : ''; $product_data['length'] = ( $product->get_length() ) ? $product->get_length() : ''; $product_data['width'] = ( $product->get_width() ) ? $product->get_width() : ''; // Featured Image if ( has_post_thumbnail( $product_data['id'] ) ) { // Check if user would like to use the mother main image for all variation products $use_parent_variable_product_image = get_option( 'adt_use_parent_variable_product_image' ); $product_image_id = 'yes' == $use_parent_variable_product_image && $product_data['item_group_id'] > 0 ? $product_data['item_group_id'] : $product_data['id']; $thumbnail_id = (int) get_post_thumbnail_id( $product_image_id ); // When min image size validation is in effect and the thumbnail is undersized // (or absent — a zero ID is treated as failing the size check), reuse the // validated swap from earlier so the feed doesn't ship an image Google/Facebook // would reject. attachment_meets_min_size() returns false for IDs <= 0 by design. if ( $image_size_validation_in_effect && ! \AdTribes\PFP\Helpers\Image_Size_Validator::attachment_meets_min_size( $thumbnail_id, $image_size_min_dimensions['width'], $image_size_min_dimensions['height'] ) && ! empty( $validated_image_url ) ) { $product_data['feature_image'] = $this->get_image_url( $validated_image_url ); } else { $image = wp_get_attachment_image_src( $thumbnail_id, 'single-post-thumbnail' ); if ( ! empty( $image[0] ) ) { $product_data['feature_image'] = $this->get_image_url( $image[0] ); } } } else { $product_data['feature_image'] = $this->get_image_url( $product_data['image'] ); } foreach ( $diff_taxonomies as $taxo ) { $term_value = get_the_terms( $product_data['id'], $taxo ); $product_data[ "$taxo" ] = ''; if ( is_array( $term_value ) ) { // Do not add variation values to the feed when they are out of stock if ( $feed_channel['fields'] == 'skroutz' ) { if ( ( $product->is_type( 'variable' ) ) && ( $product_data['item_group_id'] == 0 ) ) { $product_skroutz = wc_get_product( $product_data['id'] ); $variations = $product_skroutz->get_available_variations(); $variations_id = wp_list_pluck( $variations, 'variation_id' ); $skroutz_att_array = array(); foreach ( $variations_id as $var_id ) { $stock_value = get_post_meta( $var_id, '_stock_status', true ); if ( $stock_value == 'instock' ) { foreach ( $term_value as $term ) { $attr_value = get_post_meta( $var_id, 'attribute_' . $term->taxonomy, true ); if ( ! in_array( $attr_value, $skroutz_att_array ) ) { array_push( $skroutz_att_array, $attr_value ); } } } } $product_data[ $taxo ] = implode( ', ', $skroutz_att_array ); } else { // Simple Skroutz product $product_data[ $taxo ] = implode( ', ', wp_list_pluck( $term_value, 'name' ) ); } } else { $product_data[ $taxo ] = implode( ', ', wp_list_pluck( $term_value, 'name' ) ); } } // unset($term_value); } /* * Add product tags to the product data array */ $product_tags = get_the_terms( $product_data['id'], 'product_tag' ); if ( is_array( $product_tags ) ) { foreach ( $product_tags as $term ) { if ( ! array_key_exists( 'product_tag', $product_data ) ) { $product_data['product_tag'] = array( $term->name ); $product_data['product_tag_space'] = array( $term->name ); } else { array_push( $product_data['product_tag'], $term->name ); array_push( $product_data['product_tag_space'], $term->name ); } } } else { $product_data['product_tag'] = array(); $product_data['product_tag_space'] = array(); } // unset($product_tags); /** * Get Custom Attributes for Single, Bundled and Composite products */ if ( ( $product->is_type( 'simple' ) ) || ( $product->is_type( 'woosb' ) ) || ( $product->is_type( 'mix-and-match' ) ) || ( $product->is_type( 'external' ) ) || ( $product->is_type( 'bundle' ) ) || ( $product->is_type( 'composite' ) ) || ( $product_data['product_type'] == 'variable' ) || ( $product_data['product_type'] == 'auction' ) || ( $product->is_type( 'subscription' ) || ( $product->is_type( 'grouped' ) ) ) ) { $custom_attributes = $this->get_custom_attributes( $product_data['id'] ); if ( ! is_array( $custom_attributes ) ) { $custom_attributes = array(); } if ( ! in_array( 'woosea optimized title', $custom_attributes ) ) { $woosea_opt = array( '_woosea_optimized_title' => 'woosea optimized title', ); $custom_attributes = array_merge( $custom_attributes, $woosea_opt ); } if ( class_exists( 'All_in_One_SEO_Pack' ) ) { $custom_attributes['_aioseop_title'] = 'All in one seo pack title'; $custom_attributes['_aioseop_description'] = 'All in one seo pack description'; } if ( class_exists( 'Yoast_WooCommerce_SEO' ) ) { if ( array_key_exists( 'yoast_gtin8', $custom_attributes ) ) { $product_data['yoast_gtin8'] = $custom_attributes['yoast_gtin8']; } if ( array_key_exists( 'yoast_gtin12', $custom_attributes ) ) { $product_data['yoast_gtin12'] = $custom_attributes['yoast_gtin12']; } if ( array_key_exists( 'yoast_gtin13', $custom_attributes ) ) { $product_data['yoast_gtin13'] = $custom_attributes['yoast_gtin13']; } if ( array_key_exists( 'yoast_gtin14', $custom_attributes ) ) { $product_data['yoast_gtin14'] = $custom_attributes['yoast_gtin14']; } if ( array_key_exists( 'yoast_isbn', $custom_attributes ) ) { $product_data['yoast_isbn'] = $custom_attributes['yoast_isbn']; } if ( array_key_exists( 'yoast_mpn', $custom_attributes ) ) { $product_data['yoast_mpn'] = $custom_attributes['yoast_mpn']; } } foreach ( $custom_attributes as $custom_kk => $custom_vv ) { $custom_value = get_post_meta( $product_data['id'], $custom_kk, true ); $new_key = 'custom_attributes_' . $custom_kk; // This is a ACF image field (PLEASE NOTE: the ACF field needs to contain image or bild in the name) if ( preg_match( '/image|bild|immagine/i', $custom_kk ) ) { if ( class_exists( 'ACF' ) && ( $custom_value > 0 ) ) { $image = wp_get_attachment_image_src( $custom_value, 'large' ); if ( isset( $image[0] ) ) { $custom_value = $image[0]; } } } // Just to make sure the title is never empty if ( ( $custom_kk == '_aioseop_title' ) && ( $custom_value == '' ) ) { $custom_value = $product_data['title']; } // Just to make sure the description is never empty if ( ( $custom_kk == '_aioseop_description' ) && ( $custom_value == '' ) ) { $custom_value = $product_data['description']; } // Just to make sure product names are never empty if ( ( $custom_kk == '_woosea_optimized_title' ) && ( $custom_value == '' ) ) { $custom_value = $product_data['title']; } // Just to make sure the condition field is never empty if ( ( $custom_kk == '_woosea_condition' ) && ( $custom_value == '' ) ) { $custom_value = $product_data['condition']; } $product_data[ $new_key ] = $custom_value; } // unset($custom_attributes); /** * We need to check if this product has individual custom product attributes */ global $wpdb; $sql = $wpdb->prepare( "SELECT meta.meta_id, meta.meta_key as name, meta.meta_value as type FROM {$wpdb->prefix}postmeta AS meta, {$wpdb->prefix}posts AS posts WHERE meta.post_id = %d AND meta.post_id = posts.id GROUP BY meta.meta_key ORDER BY meta.meta_key ASC", absint( $product_data['id'] ) ); $data = $wpdb->get_results( $sql ); if ( count( $data ) ) { foreach ( $data as $key => $value ) { $value_display = str_replace( '_', ' ', $value->name ); if ( preg_match( '/_product_attributes/i', $value->name ) ) { $product_attr = unserialize( $value->type ); if ( ! empty( $product_attr ) ) { foreach ( $product_attr as $key => $arr_value ) { $new_key = 'custom_attributes_' . $key; if ( ! empty( $arr_value['value'] ) ) { $product_data[ $new_key ] = $arr_value['value']; } } } } } } // unset($data); } /** * Get Product Attributes for Single products * These are the attributes users create themselves in WooCommerce */ if ( ( $product->is_type( 'simple' ) ) || ( $product->is_type( 'external' ) ) || ( $product->is_type( 'woosb' ) ) || ( $product->is_type( 'mix-and-match' ) ) || ( $product->is_type( 'bundle' ) ) || ( $product->is_type( 'composite' ) ) || ( $product->is_type( 'auction' ) || ( $product->is_type( 'subscription' ) ) || ( $product->is_type( 'variable' ) ) ) ) { $single_attributes = $product->get_attributes(); foreach ( $single_attributes as $attribute ) { $attr_name = strtolower( $attribute->get_name() ); $attr_value = $product->get_attribute( $attr_name ); $product_data[ $attr_name ] = $attr_value; } // unset($single_attributes); } // Check if user would like to use the mother main image for all variation products $use_parent_variable_product_image = get_option( 'adt_use_parent_variable_product_image' ); if ( ( $use_parent_variable_product_image == 'yes' ) && ( $product_data['item_group_id'] > 0 ) ) { $mother_thumbnail_id = (int) get_post_thumbnail_id( $product_data['item_group_id'] ); // When min image size validation is in effect and the parent thumbnail is // undersized (or absent — a zero ID is treated as failing the size check), // keep the already-validated image from the earlier swap rather than // re-introducing an image that Google/Facebook will reject. // attachment_meets_min_size() returns false for IDs <= 0 by design. if ( $image_size_validation_in_effect && ! \AdTribes\PFP\Helpers\Image_Size_Validator::attachment_meets_min_size( $mother_thumbnail_id, $image_size_min_dimensions['width'], $image_size_min_dimensions['height'] ) ) { // Leave $product_data['image'] as-is (the validated swap). } else { $mother_image = wp_get_attachment_image_src( $mother_thumbnail_id, 'full' ); if ( isset( $mother_image[0] ) ) { $product_data['image'] = $mother_image[0]; } } } /** * Versioned products need a seperate approach * Get data for these products based on the mother products item group id */ $variation_pass = 'true'; if ( ( $product_data['item_group_id'] > 0 ) && ( is_object( wc_get_product( $product_data['item_group_id'] ) ) ) && ( ( $product_data['product_type'] == 'variation' ) || ( $product_data['product_type'] == 'subscription_variation' ) ) ) { $product_variations = new WC_Product_Variation( $product_data['id'] ); $variations = $product_variations->get_variation_attributes(); // For Skroutz and Bestprice apparal products we can only append colours to the product name // When a product has both a size and color attribute we assume its an apparal product if ( ( $feed_channel['fields'] == 'skroutz' ) || ( $feed_channel['fields'] == 'bestprice' ) ) { $size_found = 'no'; $color_found = 'no'; foreach ( $feed_attributes as $ky => $vy ) { if ( isset( $vy['attribute'] ) ) { if ( $vy['attribute'] == 'size' ) { $size_found = 'yes'; $sz_attribute = $vy['mapfrom']; } if ( $vy['attribute'] == 'color' ) { $color_found = 'yes'; $clr_attribute = $vy['mapfrom']; } } } // Remove size from variations array if ( ( $size_found == 'yes' ) && ( $color_found == 'yes' ) ) { update_option( 'skroutz_apparel', false ); update_option( 'skroutz_clr', $clr_attribute, false ); update_option( 'skroutz_sz', $sz_attribute, false ); } $skroutz_apparal = get_option( 'skroutz_apparel' ); if ( $skroutz_apparal == 'yes' ) { if ( isset( $clr_attribute ) ) { $skroutz_color = get_post_meta( $product_data['id'], 'attribute_' . $clr_attribute, true ); } if ( isset( $sz_attribute ) ) { $skroutz_size = get_post_meta( $product_data['id'], 'attribute_' . $sz_attribute, true ); } if ( ( ! empty( $skroutz_color ) ) && ( ! empty( $skroutz_size ) ) ) { foreach ( $variations as $kvar => $vvar ) { // Does this product have a color value $var_key = get_option( 'skroutz_clr' ); $var_key = 'attribute_' . $var_key; $skroutz_color = get_post_meta( $product_data['id'], $var_key, true ); // Does this color have a size value $var_key_sz = get_option( 'skroutz_sz' ); $var_key_sz = 'attribute_' . $var_key_sz; $skroutz_size = get_post_meta( $product_data['id'], $var_key_sz, true ); if ( $kvar == $var_key ) { if ( ! isset( $skroutz_clr_array ) ) { if ( ! empty( $skroutz_color ) ) { $skroutz_clr_array = array( $skroutz_color ); } } elseif ( ! empty( $skroutz_color ) ) { if ( ! in_array( $skroutz_color, $skroutz_clr_array ) ) { array_push( $skroutz_clr_array, $skroutz_color ); $variation_pass = 'true'; } else { $variation_pass = 'false'; } } } else { unset( $variations[ $kvar ] ); } } } else { // This is not an apparal product so a color variation is not allowed $variation_pass = 'true'; } } else { $variation_pass = 'true'; } } if ( ( $feed->only_include_lowest_product_variation ) || ( $feed->only_include_default_product_variation ) ) { // Determine the default variation product if ( ( $product_data['item_group_id'] > 0 ) && ( is_object( wc_get_product( $product_data['item_group_id'] ) ) ) && ( ( $product_data['product_type'] == 'variation' ) || ( $product_data['product_type'] == 'subscription_variation' ) ) ) { $mother_product = new WC_Product_Variable( $product_data['item_group_id'] ); // $mother_product = wc_get_product($product_data['item_group_id']); $def_attributes = $mother_product->get_default_attributes(); if ( $feed->only_include_lowest_product_variation ) { // Determine lowest priced variation $variation_min_price = $mother_product->get_variation_price( 'min' ); $variation_min_price = wc_format_decimal( $variation_min_price, 2 ); $variation_min_price = wc_format_localized_price( $variation_min_price ); $var_price = get_post_meta( $product_data['id'], '_price', true ); $var_price = wc_format_decimal( $var_price, 2 ); $var_price = wc_format_localized_price( $var_price ); $variation_prices = $mother_product->get_variation_prices(); $variation_prices_price = array_values( $variation_prices['price'] ); if ( ! empty( $variation_prices_price ) ) { $lowest_price = min( $variation_prices_price ); } else { $lowest_price = 0; } if ( ( $var_price == $lowest_price ) || ( $var_price == $variation_min_price ) || ( $product_data['system_regular_price'] == $variation_min_price ) || ( $product_data['system_net_price'] == $variation_min_price ) ) { $variation_pass = 'true'; } else { $variation_pass = 'false'; } } } // Get review rating and count for parent product $product_data['rating_total'] = $mother_product->get_rating_count(); $product_data['rating_average'] = $mother_product->get_average_rating(); if ( $feed->only_include_default_product_variation ) { $diff_result = array_diff( $variations, $def_attributes ); if ( ! empty( $diff_result ) ) { // Only when a variant has no attributes selected we will let it pass if ( count( array_filter( $variations ) ) == 0 ) { $variation_pass = 'true'; } else { $variation_pass = 'false'; } } } } $append = ''; $product_data['parent_sku'] = get_post_meta( $product_data['item_group_id'], '_sku', true ); /** * Add the product visibility values for variations based on the simple mother product */ $product_data['exclude_from_catalog'] = 'no'; $product_data['exclude_from_search'] = 'no'; $product_data['exclude_from_all'] = 'no'; $visibility_list = wp_get_post_terms( $product_data['item_group_id'], 'product_visibility', array( 'fields' => 'all' ) ); if ( ! empty( $visibility_list ) ) { foreach ( $visibility_list as $visibility_single ) { if ( $visibility_single->slug == 'exclude-from-catalog' ) { $product_data['exclude_from_catalog'] = 'yes'; } if ( $visibility_single->slug == 'exclude-from-search' ) { $product_data['exclude_from_search'] = 'yes'; } } } // unset($visibility_list); if ( ( $product_data['exclude_from_search'] == 'yes' ) && ( $product_data['exclude_from_catalog'] == 'yes' ) ) { $product_data['exclude_from_all'] = 'yes'; } /** * Although this is a product variation we also need to grap the Product attributes belonging to the simple mother product */ $mother_attributes = get_post_meta( $product_data['item_group_id'], '_product_attributes' ); if ( ! empty( $mother_attributes ) ) { foreach ( $mother_attributes as $attribute ) { foreach ( $attribute as $key => $attr ) { $attr_name = $attr['name']; if ( ! empty( $attr_name ) ) { $terms = get_the_terms( $product_data['item_group_id'], $attr_name ); if ( is_array( $terms ) ) { foreach ( $terms as $term ) { $attr_value = $term->name; } $product_data[ $attr_name ] = $attr_value; } else { // Add the variable parent attributes // When the attribute was not set for variations if ( $attr['is_variation'] == 0 ) { $new_key = 'custom_attributes_' . $key; $product_data[ $new_key ] = $attr['value']; } } } } } } // unset($mother_attributes); /** * Although this is a product variation we also need to grap the Dynamic attributes belonging to the simple mother prodict */ $stock_value = get_post_meta( $product_data['id'], '_stock_status', true ); // if($stock_value == "instock"){ foreach ( $diff_taxonomies as $taxo ) { $term_value = get_the_terms( $product_data['item_group_id'], $taxo ); unset( $product_data[ $taxo ] ); if ( is_array( $term_value ) ) { $product_data[ $taxo ] = implode( ', ', wp_list_pluck( $term_value, 'name' ) ); } } /** * Add product tags to the product data array */ $product_tags = get_the_terms( $product_data['item_group_id'], 'product_tag' ); if ( is_array( $product_tags ) ) { foreach ( $product_tags as $term ) { if ( ! array_key_exists( 'product_tag', $product_data ) ) { $product_data['product_tag'] = array( $term->name ); } else { array_push( $product_data['product_tag'], $term->name ); } } } // Add attribute values to the variation product names to make them unique $product_data['title_hyphen'] = $product_data['title'] . ' - '; $product_data['mother_title_hyphen'] = $product_data['mother_title'] . ' - '; foreach ( $variations as $kk => $vv ) { $custom_key = $kk; if ( $feed->include_product_variations ) { $taxonomy = str_replace( 'attribute_', '', $kk ); $term = get_term_by( 'slug', $vv, $taxonomy ); if ( $term && $term->name ) { $vv = $term->name; } if ( $vv ) { $append = ucfirst( $vv ); $append = rawurldecode( $append ); // Prevent duplicate attribute values from being added to the product name if ( ! preg_match( '/' . preg_quote( $product_data['title'], '/' ) . '/', $append ) ) { $product_data['title'] = $product_data['title'] . ' ' . $append; $product_data['title_hyphen'] = $product_data['title_hyphen'] . ' ' . $append; $product_data['raw_title'] = $product_data['raw_title'] . ' ' . $append; } } } $custom_key = str_replace( 'attribute_', '', $custom_key ); $product_data[ $custom_key ] = $vv; $append = ''; } /** * Get Custom Attributes for this variable product */ $custom_attributes = $this->get_custom_attributes( $product_data['id'] ); if ( ! is_array( $custom_attributes ) ) { $custom_attributes = array(); } if ( ! in_array( 'woosea optimized title', $custom_attributes ) ) { $woosea_opt = array( '_woosea_optimized_title' => 'woosea optimized title', ); $custom_attributes = array_merge( $custom_attributes, $woosea_opt ); } if ( class_exists( 'All_in_One_SEO_Pack' ) ) { $custom_attributes['_aioseop_title'] = 'All in one seo pack title'; $custom_attributes['_aioseop_description'] = 'All in one seo pack description'; } if ( class_exists( 'Yoast_WooCommerce_SEO' ) ) { $yoast_identifiers = get_post_meta( $product_data['id'], 'wpseo_variation_global_identifiers_values' ); if ( ! empty( $yoast_identifiers[0] ) ) { if ( array_key_exists( 'gtin8', $yoast_identifiers[0] ) ) { $product_data['yoast_gtin8'] = $yoast_identifiers[0]['gtin8']; } if ( array_key_exists( 'gtin12', $yoast_identifiers[0] ) ) { $product_data['yoast_gtin12'] = $yoast_identifiers[0]['gtin12']; } if ( array_key_exists( 'gtin13', $yoast_identifiers[0] ) ) { $product_data['yoast_gtin13'] = $yoast_identifiers[0]['gtin13']; } if ( array_key_exists( 'gtin14', $yoast_identifiers[0] ) ) { $product_data['yoast_gtin14'] = $yoast_identifiers[0]['gtin14']; } if ( array_key_exists( 'isbn', $yoast_identifiers[0] ) ) { $product_data['yoast_isbn'] = $yoast_identifiers[0]['isbn']; } if ( array_key_exists( 'mpn', $yoast_identifiers[0] ) ) { $product_data['yoast_mpn'] = $yoast_identifiers[0]['mpn']; } } } foreach ( $custom_attributes as $custom_kk => $custom_vv ) { $custom_value = get_post_meta( $product_data['id'], $custom_kk, true ); // Product variant brand is empty, grap that of the mother product if ( ( $custom_kk == '_woosea_brand' ) && ( $custom_value == '' ) ) { $custom_value = get_post_meta( $product_data['item_group_id'], $custom_kk, true ); } // Just to make sure the title is never empty if ( ( $custom_kk == '_aioseop_title' ) && ( $custom_value == '' ) ) { $custom_value = $product_data['title']; } // Just to make sure the description is never empty if ( ( $custom_kk == '_aioseop_description' ) && ( $custom_value == '' ) ) { $custom_value = $product_data['description']; } // Product variant optimized title is empty, grap the mother product title if ( ( $custom_kk == '_woosea_optimized_title' ) && ( $custom_value == '' ) ) { $custom_value = $product_data['title']; } if ( ! is_array( $custom_value ) ) { $custom_kk = str_replace( 'attribute_', '', $custom_kk ); $new_key = 'custom_attributes_' . $custom_kk; // #567: do NOT re-enable the str_replace( 'var', 'product' ) below - it corrupts // any variation meta key containing 'var' (e.g. ..._variation_1 -> ..._productiation_1), // so the value no longer matches the raw custom_attributes_ mapping key and is dropped. // $new_key = str_replace( 'var', 'product', $new_key ); if ( ! empty( $custom_value ) ) { $product_data[ $new_key ] = $custom_value; } } } /** * We need to check if this product has individual custom product attributes */ global $wpdb; $sql = $wpdb->prepare( "SELECT meta.meta_id, meta.meta_key as name, meta.meta_value as type FROM {$wpdb->prefix}postmeta AS meta, {$wpdb->prefix}posts AS posts WHERE meta.post_id = %d AND meta.post_id = posts.id GROUP BY meta.meta_key ORDER BY meta.meta_key ASC", absint( $product_data['id'] ) ); $data = $wpdb->get_results( $sql ); if ( count( $data ) ) { foreach ( $data as $key => $value ) { $value_display = str_replace( '_', ' ', $value->name ); if ( preg_match( '/_product_attributes/i', $value->name ) ) { $product_attr = unserialize( $value->type ); if ( ( ! empty( $product_attr ) ) && ( is_array( $product_attr ) ) ) { foreach ( $product_attr as $key => $arr_value ) { $new_key = 'custom_attributes_' . $key; $product_data[ $new_key ] = $arr_value['value']; } } } } } /** * We also need to make sure that we get the custom attributes belonging to the simple mother product */ $custom_attributes_mother = $this->get_custom_attributes( $product_data['item_group_id'] ); foreach ( $custom_attributes_mother as $custom_kk_m => $custom_value_m ) { if ( ! array_key_exists( $custom_kk_m, $product_data ) ) { $custom_value_m = get_post_meta( $product_data['item_group_id'], $custom_kk_m, true ); $new_key_m = 'custom_attributes_' . $custom_kk_m; if ( ! is_array( $custom_value_m ) ) { // #567: do NOT re-enable the str_replace( 'var', 'product' ) below - it corrupts // any meta key containing 'var', so the value no longer matches the raw // custom_attributes_ mapping key and is dropped. // $new_key_m = str_replace("var","product",$new_key_m); if ( ! key_exists( $new_key_m, $product_data ) && ( ! empty( $custom_value_m ) ) ) { if ( is_array( $custom_value_m ) ) { // determine what to do with this later } else { // This is most likely a ACF field if ( class_exists( 'ACF' ) && ( $custom_value_m > 0 ) ) { $image = wp_get_attachment_image_src( $custom_value_m, 'large' ); if ( isset( $image[0] ) ) { $custom_value = $image[0]; $product_data[ $new_key_m ] = $custom_value; } else { $product_data[ $new_key_m ] = $custom_value_m; } } else { $product_data[ $new_key_m ] = $custom_value_m; } } } } else { $arr_value = ''; foreach ( $custom_value_m as $key => $value ) { // not for multidimensional arrays if ( is_string( $value ) ) { if ( is_string( $value ) ) { $arr_value .= $value . ','; } } } $arr_value = rtrim( $arr_value, ',' ); $product_data[ $new_key_m ] = $arr_value; } } } // unset($custom_attributes_mother); // unset($product_variations); // unset($variations); } // END VARIABLE PRODUCT CODE /** * In order to prevent XML formatting errors in Google's Merchant center * we will add CDATA brackets to the title and description attributes */ $product_data['title_lc'] = ucfirst( strtolower( $product_data['title'] ) ); $product_data['title_lcw'] = ucwords( strtolower( $product_data['title'] ) ); /** * Get product reviews for Google Product Review Feeds */ $product_data['reviews'] = $this->woosea_get_reviews( $product_data, $product, $feed ); /** * Filter out reviews that do not have text */ if ( ! empty( $product_data['reviews'] ) ) { foreach ( $product_data['reviews'] as $review_id => $review_details ) { if ( empty( $review_details['content'] ) ) { unset( $product_data['reviews'][ $review_id ] ); } } } /** * Filter out reviews that do not have a rating */ if ( ! empty( $product_data['reviews'] ) ) { foreach ( $product_data['reviews'] as $review_id => $review_details ) { if ( empty( $review_details['review_ratings'] ) ) { unset( $product_data['reviews'][ $review_id ] ); } } } /** * Filter out reviews that have a link in the review text / content as that is now allowed by Google */ if ( ! empty( $product_data['reviews'] ) ) { foreach ( $product_data['reviews'] as $review_id => $review_details ) { if ( str_contains( $review_details['content'], 'www' ) ) { unset( $product_data['reviews'][ $review_id ] ); } if ( str_contains( $review_details['content'], 'http' ) ) { unset( $product_data['reviews'][ $review_id ] ); } } } /** * Filter out revieuws with a low rating */ if ( ! empty( $product_data['reviews'] ) ) { // Check if we need to filter uit reviews with a low rating if ( ! empty( $feed_filters ) ) { foreach ( $feed_filters as $filter_id => $filter_details ) { if ( array_key_exists( 'attribute', $filter_details ) ) { if ( $filter_details['attribute'] == 'review_rating' ) { // Loop through reviews foreach ( $product_data['reviews'] as $review_id => $review_details ) { if ( ! empty( $review_details['review_ratings'] ) ) { if ( ( $filter_details['condition'] == '<' ) && ( $review_details['review_ratings'] < $filter_details['criteria'] ) && ( $filter_details['than'] == 'exclude' ) ) { unset( $product_data['reviews'][ $review_id ] ); } elseif ( ( $filter_details['condition'] == '>' ) && ( $review_details['review_ratings'] > $filter_details['criteria'] ) && ( $filter_details['than'] == 'exclude' ) ) { unset( $product_data['reviews'][ $review_id ] ); } elseif ( ( $filter_details['condition'] == '>=' ) && ( $review_details['review_ratings'] >= $filter_details['criteria'] ) && ( $filter_details['than'] == 'exclude' ) ) { unset( $product_data['reviews'][ $review_id ] ); } } } } } } } } /** * Check if individual products need to be excluded */ $product_data = $this->woosea_exclude_individual( $product_data ); /** * Do final check on Skroutz out of stock sizes * When a size is not on stock remove it */ if ( $feed_channel['fields'] == 'skroutz' ) { if ( isset( $product_data['id'] ) ) { foreach ( $feed_attributes as $ky => $vy ) { if ( isset( $vy['attribute'] ) ) { if ( $vy['attribute'] == 'size' ) { $size_found = 'yes'; $sz_attribute = $vy['mapfrom']; } if ( $vy['attribute'] == 'color' ) { $color_found = 'yes'; $clr_attribute = $vy['mapfrom']; } } } $stock_value = get_post_meta( $product_data['id'], '_stock_status', true ); if ( ! empty( $clr_attribute ) ) { $clr_attr_value = get_post_meta( $product_data['id'], 'attribute_' . $clr_attribute, true ); } else { $clr_attr_value = ''; } if ( isset( $product_data['item_group_id'] ) && ( $product_data['product_type'] == 'variation' ) ) { if ( $product_data['item_group_id'] > 0 ) { $product_skroutz = wc_get_product( $product_data['item_group_id'] ); if ( is_object( $product_skroutz ) ) { $skroutz_product_type = $product_skroutz->get_type(); } if ( ( $product_skroutz ) && ( $skroutz_product_type == 'variable' ) ) { $variations = $product_skroutz->get_available_variations(); $variations_id = wp_list_pluck( $variations, 'variation_id' ); $total_quantity = 0; $quantity_variation = 0; $sizez = array(); foreach ( $variations_id as $var_id_s ) { $taxonomy = 'pa_size'; $sizez_variation = get_post_meta( $var_id_s, 'attribute_' . $taxonomy, true ); if ( $sizez_variation ) { $sizez_term = get_term_by( 'slug', $sizez_variation, $taxonomy ); if ( ! in_array( $sizez_term->name, $sizez ) ) { array_push( $sizez, $sizez_term->name ); } } } foreach ( $variations_id as $var_id ) { if ( isset( $clr_attribute ) ) { // $clr_variation = get_post_meta( $product_data['id'], "attribute_".$clr_attribute, true ); $clr_variation = get_post_meta( $var_id, 'attribute_' . $clr_attribute, true ); } else { $clr_variation = ''; } // Sum quantity of variations for apparel products if ( array_key_exists( 'pa_size', $product_data ) && array_key_exists( 'pa_color', $product_data ) ) { $quantity_variation = $this->get_attribute_value( $var_id, '_stock' ); if ( ! empty( $quantity_variation ) ) { $total_quantity += $quantity_variation; } $product_data['quantity'] = $total_quantity; } if ( isset( $sz_attribute ) ) { $size_variation = ucfirst( get_post_meta( $var_id, 'attribute_' . $sz_attribute, true ) ); } $stock_variation = get_post_meta( $var_id, '_stock_status', true ); if ( $clr_variation == $clr_attr_value ) { if ( $stock_variation == 'outofstock' ) { // Remove this size as it is not on stock $size_variation_new = $size_variation . ','; $size_variation_new = str_replace( '-', ' ', $size_variation_new ); $size_variation = str_replace( '-', ' ', $size_variation ); if ( isset( $sz_attribute ) ) { if ( array_key_exists( $sz_attribute, $product_data ) ) { $product_data[ $sz_attribute ] = str_replace( ucfirst( $size_variation ), '', $product_data[ $sz_attribute ] ); $product_data[ $sz_attribute ] = rtrim( $product_data[ $sz_attribute ], ' ' ); $product_data[ $sz_attribute ] = trim( $product_data[ $sz_attribute ], ',' ); } } } else { // Add comma's in the size field and put availability on stock as at least one variation is on stock if ( isset( $size_variation ) ) { if ( isset( $sz_attribute ) ) { $product_data[ $sz_attribute ] = implode( ', ', $sizez ); $product_data['availability'] = 'in stock'; } } } } } } } else { // This is a parent variable product if ( $product_data['product_type'] == 'variable' ) { $product_skroutz = wc_get_product( $product_data['id'] ); $variations = $product_skroutz->get_available_variations(); $variations_id = wp_list_pluck( $variations, 'variation_id' ); foreach ( $variations_id as $var_id ) { // $clr_variation = get_post_meta( $var_id, "attribute_".$clr_attribute, true ); $size_variation = get_post_meta( $var_id, 'attribute_' . $sz_attribute, true ); $stock_variation = get_post_meta( $var_id, '_stock_status', true ); if ( $stock_variation == 'outofstock' ) { // Remove this size as it is not on stock if ( array_key_exists( $sz_attribute, $product_data ) ) { $product_data[ $sz_attribute ] = str_replace( ucfirst( $size_variation ), '', $product_data[ $sz_attribute ] ); $product_data[ $sz_attribute ] = str_replace( ', , ', ',', $product_data[ $sz_attribute ] ); $product_data[ $sz_attribute ] = rtrim( $product_data[ $sz_attribute ], ' ' ); $product_data[ $sz_attribute ] = rtrim( $product_data[ $sz_attribute ], ',' ); } } } } } } elseif ( $product_data['product_type'] == 'variable' ) { $product_skroutz = wc_get_product( $product_data['id'] ); $variations = $product_skroutz->get_available_variations(); $variations_id = wp_list_pluck( $variations, 'variation_id' ); $enabled_sizes = array(); foreach ( $variations_id as $var_id ) { if ( isset( $sz_attribute ) ) { $size_variation = strtoupper( get_post_meta( $var_id, 'attribute_' . $sz_attribute, true ) ); $enabled_sizes[] = $size_variation; } } $new_size = ''; foreach ( $enabled_sizes as $siz ) { $siz = trim( $siz, ' ' ); $size_variation = trim( $size_variation, ' ' ); $new_size .= ' ' . $siz . ','; } if ( isset( $sz_attribute ) ) { $product_data[ $sz_attribute ] = $new_size; $product_data[ $sz_attribute ] = str_replace( ', , ', ',', $product_data[ $sz_attribute ] ); $product_data[ $sz_attribute ] = rtrim( $product_data[ $sz_attribute ], ' ' ); $product_data[ $sz_attribute ] = rtrim( $product_data[ $sz_attribute ], ',' ); $product_data[ $sz_attribute ] = ltrim( $product_data[ $sz_attribute ], ',' ); } foreach ( $variations_id as $var_id ) { if ( isset( $sz_attribute ) ) { $size_variation = get_post_meta( $var_id, 'attribute_' . $sz_attribute, true ); $product_excluded = ucfirst( get_post_meta( $var_id, '_woosea_exclude_product', true ) ); if ( $product_excluded == 'Yes' ) { // Remove this size as it is has been set to be excluded from feeds if ( array_key_exists( $sz_attribute, $product_data ) ) { $new_size = ''; foreach ( $enabled_sizes as $siz ) { $siz = trim( $siz, ' ' ); $size_variation = trim( $size_variation, ' ' ); if ( $siz != strtoupper( $size_variation ) ) { $new_size .= ' ' . $siz . ','; } } $product_data[ $sz_attribute ] = $new_size; $product_data[ $sz_attribute ] = str_replace( ', , ', ',', $product_data[ $sz_attribute ] ); $product_data[ $sz_attribute ] = rtrim( $product_data[ $sz_attribute ], ' ' ); $product_data[ $sz_attribute ] = rtrim( $product_data[ $sz_attribute ], ',' ); $product_data[ $sz_attribute ] = ltrim( $product_data[ $sz_attribute ], ',' ); } } } } } } } if ( isset( $product_data['title_lcw'] ) ) { $product_data['title_lcw'] = ucwords( $product_data['title_lcw'] ); } /** * When a product is a variable product we need to delete the original product from the feed, only the originals are allowed */ // For these channels parent products are allowed $allowed_channel_parents = array( 'skroutz', // "bestprice", 'google_dsa', 'google_product_review', ); if ( ! in_array( $feed_channel['fields'], $allowed_channel_parents ) ) { if ( ( $product->is_type( 'variable' ) ) && ( isset( $product_data['item_group_id'] ) ) ) { $product_data = array(); $product_data = null; } } /** * Remove variation products that are not THE default variation product */ if ( ( isset( $variation_pass ) ) && ( $variation_pass == 'false' ) ) { $product_data = array(); $product_data = null; } /** * And item_group_id is not allowed for simple products, prevent users from adding this to the feedd */ if ( ( $product->is_type( 'simple' ) ) || ( $product->is_type( 'external' ) ) || ( $product->is_type( 'woosb' ) ) || ( $product->is_type( 'mix-and-match' ) ) || ( $product->is_type( 'bundle' ) ) || ( $product->is_type( 'composite' ) ) || ( $product->is_type( 'auction' ) || ( $product->is_type( 'subscription' ) ) || ( $product->is_type( 'variable' ) ) ) ) { unset( $product_data['item_group_id'] ); } /** * Truncate length of product title when it is over 150 characters (requirement for Google Shopping, Pinterest and Facebook */ if ( isset( $product_data['title'] ) ) { $length_title = strlen( $product_data['title'] ); if ( $length_title > 149 ) { $product_data['title'] = mb_substr( $product_data['title'], 0, 150 ); } } /** * Filter to allow manipulation of product data before it is added to the feed. * * @since 13.3.6 * * @param array $product_data The product data array * @param object $feed The feed object * @param object $product The product object */ $product_data = apply_filters( 'adt_get_product_data', $product_data, $feed, $product ); // Initialize before the guarded block so the variable is always defined for the Google Shopping check below. $categories_before_rules = null; // Filter and rules execution. if ( is_array( $product_data ) && ! empty( $product_data ) ) { // Filter execution. $filters_instance = AdTribes\PFP\Classes\Filters::instance(); $product_data = $filters_instance->filter( $product_data, $feed ); // Capture categories before rules so we can detect rule modifications below. $categories_before_rules = $product_data['categories'] ?? null; // Rules execution. $rules_instance = AdTribes\PFP\Classes\Rules::instance(); $product_data = $rules_instance->rules( $product_data, $feed ); } /** * Localize the price attributes. */ if ( ! empty( $product_data ) ) { $product_data_instance = AdTribes\PFP\Classes\Product_Data::instance(); $product_data = $product_data_instance->localize_prices( $product_data, $feed ); } /** * Check if we need to add category taxonomy mappings (Google Shopping). * Skip mapping if a rule already modified the categories value — the rule takes precedence. */ if ( ! empty( $product_data ) && ! empty( $product_data['id'] ) && $feed_channel['taxonomy'] == 'google_shopping' ) { $categories_modified_by_rules = ( $product_data['categories'] ?? null ) !== $categories_before_rules; if ( ! $categories_modified_by_rules ) { if ( ! empty( $feed_mappings ) ) { $product_data = $this->woocommerce_sea_mappings( $feed_mappings, $product_data ); } else { $product_data['categories'] = ''; } } } /** * When product has passed the filter rules it can continue with the rest */ if ( ! empty( $product_data ) ) { /** * Determine what fields are allowed to make it to the csv and txt productfeed */ if ( ( $feed_channel['fields'] != 'standard' ) && ( ! isset( $tmp_attributes ) ) && is_array( $feed_attributes ) && ! empty( $feed_attributes ) ) { $old_attributes_config = $feed_attributes; $tmp_attributes = array(); foreach ( $feed_attributes as $key => $value ) { if ( strlen( $value['mapfrom'] ) > 0 ) { $tmp_attributes[ $value['mapfrom'] ] = 'true'; } } } if ( isset( $old_attributes_config ) && is_array( $old_attributes_config ) ) { $loop_count = 0; foreach ( $old_attributes_config as $attr_key => $attr_value ) { if ( ! $attr_line ) { if ( array_key_exists( 'static_value', $attr_value ) ) { if ( strlen( $attr_value['mapfrom'] ) ) { $attr_line = "'" . $attr_value['prefix'] . $attr_value['mapfrom'] . $attr_value['suffix'] . "'"; } else { $attr_line = "''"; } } elseif ( ( strlen( $attr_value['mapfrom'] ) ) && ( array_key_exists( $attr_value['mapfrom'], $product_data ) ) ) { if ( ( $attr_value['attribute'] == 'URL' ) || ( $attr_value['attribute'] == 'g:link' ) || ( $attr_value['attribute'] == 'g:link_template' ) || ( $attr_value['attribute'] == 'g:image_link' ) || ( $attr_value['attribute'] == 'link' ) || ( $attr_value['attribute'] == 'Final URL' ) || ( $attr_value['attribute'] == 'SKU' ) || ( $attr_value['attribute'] == 'g:itemid' ) ) { $attr_line = "'" . $attr_value['prefix'] . $product_data[ $attr_value['mapfrom'] ] . $attr_value['suffix'] . "'"; } else { $attr_line = "'" . $attr_value['prefix'] . $product_data[ $attr_value['mapfrom'] ] . $attr_value['suffix'] . "'"; } } else { $attr_line = "''"; } } elseif ( array_key_exists( 'static_value', $attr_value ) ) { $attr_line .= ",'" . $attr_value['prefix'] . $attr_value['mapfrom'] . $attr_value['suffix'] . "'"; } elseif( $attr_value['mapfrom'] === 'page_url' || $attr_value['mapfrom'] === 'post_url' ) { $post_or_page_id = $attr_value['value'] ?? ''; if ( $post_or_page_id ) { $attr_line .= ",'" . $attr_value['prefix'] . get_permalink( $post_or_page_id ) . $attr_value['suffix'] . "'"; } } else { if ( array_key_exists( $attr_value['mapfrom'], $product_data ) ) { if ( is_array( $product_data[ $attr_value['mapfrom'] ] ) ) { if ( $attr_value['mapfrom'] == 'product_tag' ) { $product_tag_str = ''; foreach ( $product_data['product_tag'] as $key => $value ) { $product_tag_str .= ','; $product_tag_str .= "$value"; } $product_tag_str = rtrim( $product_tag_str, ',' ); $product_tag_str = ltrim( $product_tag_str, ',' ); $attr_line .= ",'" . $product_tag_str . "'"; } elseif ( $attr_value['mapfrom'] == 'reviews' ) { $review_str = ''; foreach ( $product_data[ $attr_value['mapfrom'] ] as $key => $value ) { $review_str .= '||'; foreach ( $value as $k => $v ) { $review_str .= ":$v"; } } $review_str = ltrim( $review_str, '||' ); $review_str = rtrim( $review_str, ':' ); $review_str = ltrim( $review_str, ':' ); $review_str = str_replace( '||:', '||', $review_str ); $review_str .= '||'; $attr_line .= ",'" . $review_str . "'"; } else { $shipping_str = ''; foreach ( $product_data[ $attr_value['mapfrom'] ] as $key => $value ) { $shipping_str .= '||'; if ( is_array( $value ) ) { foreach ( $value as $k => $v ) { if ( preg_match( '/[0-9]/', $v ) ) { $shipping_str .= ":$attr_value[prefix]" . $v . "$attr_value[suffix]"; // $shipping_str .= ":$attr_value[prefix]".$v."$attr_value[suffix]"; } else { $shipping_str .= ":$v"; } } } } $shipping_str = ltrim( $shipping_str, '||' ); $shipping_str = rtrim( $shipping_str, ':' ); $shipping_str = ltrim( $shipping_str, ':' ); $shipping_str = str_replace( '||:', '||', $shipping_str ); $attr_line .= ",'" . $shipping_str . "'"; } } elseif ( isset( $product_data[ $attr_value['mapfrom'] ] ) ) { if ( ( $attr_value['attribute'] == 'URL' ) || ( $attr_value['attribute'] == 'g:link' ) || ( $attr_value['attribute'] == 'g:link_template' ) || ( $attr_value['attribute'] == 'g:image_link' ) || ( $attr_value['attribute'] == 'link' ) || ( $attr_value['attribute'] == 'Final URL' ) || ( $attr_value['attribute'] == 'SKU' ) || ( $attr_value['attribute'] == 'g:itemid' ) ) { if ( ( $product_data['product_type'] == 'variation' ) && ( preg_match( '/aelia_cs_currency/', $attr_value['suffix'] ) ) ) { $attr_value['suffix'] = str_replace( '?', '&', $attr_value['suffix'] ); $attr_line .= ",'" . $attr_value['prefix'] . $product_data[ $attr_value['mapfrom'] ] . $attr_value['suffix'] . "'"; } elseif ( ( $product_data['product_type'] == 'variation' ) && ( preg_match( '/currency/', $attr_value['suffix'] ) ) ) { $attr_value['suffix'] = str_replace( '?', '&', $attr_value['suffix'] ); $attr_line .= ",'" . $attr_value['prefix'] . $product_data[ $attr_value['mapfrom'] ] . $attr_value['suffix'] . "'"; } else { $attr_line .= ",'" . $attr_value['prefix'] . $product_data[ $attr_value['mapfrom'] ] . $attr_value['suffix'] . "'"; } } elseif ( $product_data[ $attr_value['mapfrom'] ] !== '' ) { $attr_line .= ",'" . $attr_value['prefix'] . $product_data[ $attr_value['mapfrom'] ] . $attr_value['suffix'] . "'"; } else { $attr_line .= ",''"; } } else { $attr_line .= ",''"; } } else { $attr_line .= ",''"; } } ++$loop_count; } $pieces_row = explode( "','", $attr_line ); $pieces_row = array_map( 'trim', $pieces_row ); // Fix the first and last elements to remove the extra quotes if (!empty($pieces_row)) { // Remove leading quote from first element $pieces_row[0] = ltrim($pieces_row[0], "'"); // Remove trailing quote from last element $last_index = count($pieces_row) - 1; $pieces_row[$last_index] = rtrim($pieces_row[$last_index], "'"); } /** * Filter to allow manipulation of CSV row data before it is written to the feed. * This allows channel-specific handling of attributes in CSV format. * * @since 13.4.9 * * @param array $pieces_row The CSV row data array * @param array $old_attributes_config The feed attributes configuration * @param array $product_data The product data array * @param object $feed The feed object * @return array Modified CSV row data */ $pieces_row = apply_filters( 'adt_product_feed_csv_row_data', $pieces_row, $old_attributes_config, $product_data, $feed ); // Identifier exists attribute for any channel with Plugin Calculation Value Attribute. // Some channels (e.g. Facebook DRM) declare these without the g: namespace prefix, // so we accept both naming conventions here. $identifier_attributes = array( 'g:brand', 'g:gtin', 'g:mpn', 'brand', 'gtin', 'mpn' ); $identifier_exists_attributes = array( 'g:identifier_exists', 'identifier_exists' ); $has_identifier_exists_calculation = false; $identifier_exists_position = -1; $identifier_attributes_positions = array(); $loop_count = 0; foreach ( $old_attributes_config as $attr ) { if ( isset( $attr['attribute'] ) ) { if ( in_array( $attr['attribute'], $identifier_exists_attributes, true ) ) { $identifier_exists_position = $loop_count; if ( isset( $attr['mapfrom'] ) && $attr['mapfrom'] === 'calculated' ) { $has_identifier_exists_calculation = true; } } if ( in_array( $attr['attribute'], $identifier_attributes, true ) ) { $identifier_attributes_positions[] = $loop_count; } } $loop_count++; } // Only compute identifier_exists if the user mapped it to Plugin Calculation. if ( $has_identifier_exists_calculation && $identifier_exists_position != -1 ) { $pieces_row[ $identifier_exists_position ] = 'no'; if ( ! empty( $identifier_attributes_positions ) ) { foreach ( $identifier_attributes_positions as $identifier_attribute_position ) { if ( isset( $pieces_row[ $identifier_attribute_position ] ) && '' !== $pieces_row[ $identifier_attribute_position ] ) { $pieces_row[ $identifier_exists_position ] = 'yes'; break; } } } } $attr_line = implode( "','", $pieces_row ); $products[] = array( $attr_line ); // Track preview mode product count (CSV/TXT feeds only) if ( $is_preview_mode && $file_format != 'xml' && $file_format != 'jsonl' && $file_format != 'jsonl.gz' ) { $preview_found_count++; // Exit the inner while loop if we have enough CSV products if ( $preview_found_count >= $preview_target_count ) { break; } } } else { $attr_line = ''; if ( ! empty( $tmp_attributes ) ) { foreach ( array_keys( $tmp_attributes ) as $attribute_key ) { if ( array_key_exists( $attribute_key, $product_data ) ) { if ( ! $attr_line ) { $attr_line = "'" . $product_data[ $attribute_key ] . "'"; } else { $attr_line .= ",'" . $product_data[ $attribute_key ] . "'"; } } } } $attr_line = trim( $attr_line, "'" ); $products[] = array( $attr_line ); // Track preview mode product count (CSV/TXT feeds only) if ( $is_preview_mode && $file_format != 'xml' && $file_format != 'jsonl' && $file_format != 'jsonl.gz' ) { $preview_found_count++; // Exit the inner while loop if we have enough CSV products if ( $preview_found_count >= $preview_target_count ) { break; } } } /** * Build an array needed for the adding Childs in the XML productfeed */ $ga = 0; $ca = 0; if ( ! empty( $tmp_attributes ) ) { foreach ( array_keys( $tmp_attributes ) as $attribute_key ) { /** * This loop used to be used to skip the numeric keys. * Which can be problematic if the attributes is a static value and it has numeric value. * Because the static_value is using the $attribute_key as the key for the attribute for some fucking reason. * Instead of removing the numeric keys, we now convert them to strings. **/ if ( is_numeric( $attribute_key ) ) { $attribute_key = strval( $attribute_key ); } if ( ! isset( $old_attributes_config ) ) { if ( ! $xml_product ) { $xml_product = array( $attribute_key => $product_data[ $attribute_key ], ); } elseif ( isset( $product_data[ $attribute_key ] ) ) { $xml_product = array_merge( $xml_product, array( $attribute_key => $product_data[ $attribute_key ] ) ); } } else { foreach ( $old_attributes_config as $attr_key => $attr_value ) { $is_attr_static = array_key_exists( 'static_value', $attr_value ); $attr_map_from = $attr_value['mapfrom'] ?? ''; $attr_attribute = $attr_value['attribute'] ?? ''; // The static value is the value set by the user in the feed settings. // It's stored in the mapfrom field. // If the mapfrom field is empty, it means the value is not set by the user. // In this case, we use the value from the product data. $attr_data_value = $is_attr_static ? $attr_map_from : $product_data[ $attr_map_from ] ?? ''; /** * Special handling for page_url and post_url attributes. * * If the attribute is a page_url or post_url, we need to get the permalink of the page or post. * And then set the value to the xml product. */ if ( $attr_map_from === 'page_url' || $attr_map_from === 'post_url' ) { $post_or_page_id = $attr_value['value'] ?? ''; if ( $post_or_page_id ) { $attr_data_value = get_permalink( $post_or_page_id ); } } if ( $attr_map_from != $attribute_key ) { continue; } if ( ! isset( $xml_product ) ) { $xml_product = array( $attr_attribute => "$attr_value[prefix]" . $attr_data_value . "$attr_value[suffix]", ); } else { if ( is_array( $attr_data_value ) ) { if ( $attr_map_from == 'product_tag' ) { $product_tag_str = ''; foreach ( $product_data['product_tag'] as $key => $value ) { $product_tag_str .= ','; $product_tag_str .= "$value"; } $product_tag_str = ltrim( $product_tag_str, ',' ); $product_tag_str = rtrim( $product_tag_str, ',' ); $xml_product[ $attr_attribute ] = "$product_tag_str"; } elseif ( $attr_map_from == 'product_tag_space' ) { $product_tag_str_space = ''; foreach ( $product_data['product_tag'] as $key => $value ) { $product_tag_str_space .= ', '; $product_tag_str_space .= "$value"; } $product_tag_str_space = ltrim( $product_tag_str_space, ' ,' ); $product_tag_str_space = rtrim( $product_tag_str_space, ', ' ); $xml_product[ $attr_attribute ] = "$product_tag_str_space"; } elseif ( $attr_map_from == 'reviews' ) { $review_str = ''; foreach ( $attr_data_value as $key => $value ) { $review_str .= '||'; foreach ( $value as $k => $v ) { if ( $k == 'review_product_id' ) { $review_str .= ":::REVIEW_PRODUCT_ID##$v"; } elseif ( $k == 'reviewer_image' ) { $review_str .= ":::REVIEWER_IMAGE##$v"; } elseif ( $k == 'review_ratings' ) { $review_str .= ":::REVIEW_RATINGS##$v"; } elseif ( $k == 'review_id' ) { $review_str .= ":::REVIEW_ID##$v"; } elseif ( $k == 'reviewer_name' ) { $review_str .= ":::REVIEWER_NAME##$v"; } elseif ( $k == 'reviewer_id' ) { $review_str .= ":::REVIEWER_ID##$v"; } elseif ( $k == 'review_timestamp' ) { $v = str_replace( ' ', 'T', $v ); $v .= 'Z'; $review_str .= ":::REVIEW_TIMESTAMP##$v"; } elseif ( $k == 'review_url' ) { $review_str .= ":::REVIEW_URL##$v"; } elseif ( $k == 'title' ) { $review_str .= ":::TITLE##$v"; } elseif ( $k == 'content' ) { $review_str .= ":::CONTENT##$v"; } elseif ( $k == 'pros' ) { $review_str .= ":::PROS##$v"; } elseif ( $k == 'cons' ) { $review_str .= ":::CONS##$v"; } else { // UNKNOWN, DO NOT ADD } } } $review_str = ltrim( $review_str, '||' ); $review_str = rtrim( $review_str, ':' ); $review_str = ltrim( $review_str, ':' ); $review_str = str_replace( '||:', '||', $review_str ); $review_str .= '||'; $xml_product[ $attr_attribute ] = "$review_str"; } elseif ( $attr_map_from == 'shipping' ) { $shipping_str = ''; foreach ( $attr_data_value as $key => $value ) { $shipping_str .= '||'; foreach ( $value as $k => $v ) { if ( $k == 'country' ) { $shipping_str .= ':WOOSEA_COUNTRY##' . sanitize_text_field( $v ); } elseif ( $k == 'region' ) { $shipping_str .= ':WOOSEA_REGION##' . sanitize_text_field( $v ); } elseif ( $k == 'service' ) { $shipping_str .= ':WOOSEA_SERVICE##' . sanitize_text_field( $v ); } elseif ( $k == 'postal_code' ) { $formatted_postal_code = Product_Feed_Helper::format_shipping_postal_code( $v, isset( $value['country'] ) ? $value['country'] : '' ); $shipping_str .= ':WOOSEA_POSTAL_CODE##' . sanitize_text_field( $formatted_postal_code ); } elseif ( $k == 'price' ) { $shipping_str .= ':WOOSEA_PRICE##' . sanitize_text_field( $attr_value['prefix'] ) . sanitize_text_field( $v ) . sanitize_text_field( $attr_value['suffix'] ); } elseif ( $k == 'min_transit_time' ) { $shipping_str .= ':WOOSEA_MIN_TRANSIT_TIME##' . sanitize_text_field( $v ); } elseif ( $k == 'max_transit_time' ) { $shipping_str .= ':WOOSEA_MAX_TRANSIT_TIME##' . sanitize_text_field( $v ); } elseif ( $k == 'min_handling_time' ) { $shipping_str .= ':WOOSEA_MIN_HANDLING_TIME##' . sanitize_text_field( $v ); } elseif ( $k == 'max_handling_time' ) { $shipping_str .= ':WOOSEA_MAX_HANDLING_TIME##' . sanitize_text_field( $v ); } else { // UNKNOWN, DO NOT ADD } } } $shipping_str = ltrim( $shipping_str, '||' ); $shipping_str = rtrim( $shipping_str, ':' ); $shipping_str = ltrim( $shipping_str, ':' ); $shipping_str = str_replace( '||:', '||', $shipping_str ); $xml_product[ $attr_attribute ] = "$shipping_str"; } elseif ( $attr_map_from == 'free_shipping_threshold' ) { if ( ! is_array( $attr_data_value ) ) { continue; // Skip malformed data } $threshold_parts = []; foreach ( $attr_data_value as $key => $value ) { $threshold_part = ''; foreach ( $value as $k => $v ) { if ( $k == 'country' ) { $threshold_part .= ":WOOSEA_COUNTRY##$v"; } elseif ( $k == 'price_threshold' ) { $prefix = $attr_value['prefix'] ?? ''; $suffix = $attr_value['suffix'] ?? ''; $threshold_part .= ":WOOSEA_PRICE_THRESHOLD##$prefix$v$suffix"; } } if ( !empty( $threshold_part ) ) { $threshold_parts[] = ltrim( $threshold_part, ':' ); } } $xml_product[ $attr_attribute ] = implode( '||', $threshold_parts ); } else { // Array is returned and add to feed $arr_return = ''; if ( isset( $attr_data_value ) && is_array( $attr_data_value ) ) { foreach ( $attr_data_value as $key => $value ) { $arr_return .= $value . ','; } } $arr_return = rtrim( $arr_return, ',' ); $xml_product[ $attr_attribute ] = $arr_return; } } else { ++$ga; if ( array_key_exists( $attr_attribute, $xml_product ) ) { $ca = explode( '_', $attr_map_from ); $ca_extra = end( $ca ); // Google Shopping Actions, allow multiple product highlights, included destinations and excluded destinations in feed if ( in_array( $attr_attribute, array( 'g:product_highlight', 'g:included_destination', 'g:excluded_destination' ), true ) ) { $xml_product[ $attr_attribute . "_$ga" ] = "$attr_value[prefix]" . $attr_data_value . "$attr_value[suffix]"; } elseif ( $attr_attribute == 'g:consumer_notice' ) { $xml_product[ $attr_attribute . "_$ga" ] = "$attr_value[prefix]" . $attr_data_value . "$attr_value[suffix]"; } elseif ( $attr_attribute == 'g:product_detail' ) { $xml_product[ $attr_attribute . "_$ga" ] = "$attr_value[prefix]||" . $attr_map_from . '#' . $attr_data_value . "$attr_value[suffix]"; } elseif ( str_contains( $attr_attribute, 'g:additional_image_link' ) ) { // Skip empty additional image slots. When the product has no image // for this slot but a prefix/suffix is configured, the composed // value (e.g. "?gla090723_04") is non-empty and slips past the // output-stage empty guard, emitting an invalid node. Mirror the // guard already applied on the first-occurrence path below. if ( $attr_data_value !== '' ) { $xml_product[ $attr_attribute . "_$ca_extra" ] = "$attr_value[prefix]" . $attr_data_value . "$attr_value[suffix]"; } } else { $xml_product[ $attr_attribute . "_$ca_extra" ] = "$attr_value[prefix]" . $attr_data_value . "$attr_value[suffix]"; } } elseif ( isset( $attr_data_value ) ) { if ( $attr_attribute == 'URL' || $attr_attribute == 'g:link' || $attr_attribute == 'link' || $attr_attribute == 'g:link_template' ) { if ( $product_data['product_type'] == 'variation' && preg_match( '/aelia_cs_currency/', $attr_value['suffix'] ) ) { $attr_value['suffix'] = str_replace( '?', '&', $attr_value['suffix'] ); $xml_product[ $attr_attribute ] = "$attr_value[prefix]" . $attr_data_value . "$attr_value[suffix]"; } elseif ( $product_data['product_type'] == 'variation' && preg_match( '/currency/', $attr_value['suffix'] ) ) { $attr_value['suffix'] = str_replace( '?', '&', $attr_value['suffix'] ); $xml_product[ $attr_attribute ] = "$attr_value[prefix]" . $attr_data_value . "$attr_value[suffix]"; } else { $xml_product[ $attr_attribute ] = "$attr_value[prefix]" . $attr_data_value . "$attr_value[suffix]"; } } elseif ( ( $attr_attribute == 'g:image_link' || str_contains( $attr_attribute, 'g:additional_image_link' ) || $attr_attribute == 'image_link' ) && $attr_data_value !== '' ) { $xml_product[ $attr_attribute ] = "$attr_value[prefix]" . $attr_data_value . "$attr_value[suffix]"; } elseif ( ( $attr_attribute == 'g:id' || $attr_attribute == 'id' || $attr_attribute == 'g:itemid' ) && $attr_data_value !== '' ) { $xml_product[ $attr_attribute ] = "$attr_value[prefix]" . $attr_data_value . "$attr_value[suffix]"; } elseif ( $attr_attribute == 'g:item_group_id' && $attr_data_value !== '' ) { $xml_product[ $attr_attribute ] = "$attr_value[prefix]" . $attr_data_value . "$attr_value[suffix]"; } elseif ( $attr_attribute == 'g:consumer_notice' && $attr_data_value !== '' ) { $xml_product[ $attr_attribute ] = "$attr_value[prefix]" . $attr_data_value . "$attr_value[suffix]"; } elseif ( $attr_attribute == 'g:product_detail' && $attr_data_value !== '' ) { $xml_product[ $attr_attribute ] = "$attr_value[prefix]||" . $attr_map_from . '#' . $attr_data_value . "$attr_value[suffix]"; } elseif ( in_array( $attr_attribute, array( 'g:product_highlight', 'g:included_destination', 'g:excluded_destination' ), true ) && $attr_data_value !== '' ) { $xml_product[ $attr_attribute . "_$ga" ] = "$attr_value[prefix]" . $attr_data_value . "$attr_value[suffix]"; } elseif ( $attr_data_value !== '' ) { $xml_product[ $attr_attribute ] = "$attr_value[prefix]" . $attr_data_value . "$attr_value[suffix]"; } } } } } } } } // Do we need to do some calculation on attributes for Google Shopping $xml_product = $this->woosea_calculate_value( $feed, $xml_product ); foreach ( $xml_product as $key_product => $value_product ) { if ( preg_match( '/custom_attributes_attribute_/', $key_product ) ) { $pieces = explode( 'custom_attributes_attribute_', $key_product ); unset( $xml_product[ $key_product ] ); $xml_product[ $pieces[1] ] = $value_product; } elseif ( preg_match( '/product_attributes_/', $key_product ) ) { $pieces = explode( 'product_attributes_', $key_product ); unset( $xml_product[ $key_product ] ); $xml_product[ $pieces[1] ] = $value_product; } } // For Skroutz template if ( $feed_channel['fields'] == 'skroutz' ) { if ( $product->get_type() == 'variation' ) { $product_id = $product->get_parent_id(); $xml_piece[ $product_id ]['skroutz_variations'][] = $xml_product; } else { $xml_piece[ $product->get_id() ] = $xml_product; } } else { // For JSONL formats, allow channel-specific product-level transformations // (e.g. shipping parsing and HTML entity decoding for OpenAI). if ( $file_format === 'jsonl' || $file_format === 'jsonl.gz' ) { /** * Filter a single JSONL product array before it is added to the batch. * * @since 13.5.2 * * @param array $xml_product The product data key/value array. * @param array $feed_channel The active channel configuration. * @param object $feed The feed object. */ $xml_product = apply_filters( 'adt_product_feed_jsonl_product', $xml_product, $feed_channel, $feed ); } $xml_piece[] = $xml_product; } // Track preview mode product count (XML/JSONL feeds only) if ( $is_preview_mode && ( $file_format == 'xml' || $file_format == 'jsonl' || $file_format == 'jsonl.gz' ) ) { $preview_found_count++; // Exit the inner while loop if we have enough products if ( $preview_found_count >= $preview_target_count ) { break; } } unset( $xml_product ); unset( $product_data ); } endwhile; // Store post count before resetting query $last_query_post_count = $prods->post_count; wp_reset_query(); wp_reset_postdata(); // Advance the cursor to the last row this query walked. The query itself only // selected IDs, so the sort key of that one row is read here - a single read // per batch, not per product. // // In practice this is a cache hit rather than a query: the row loop above ran // wc_get_product() on this same ID, which primes WP's post cache. That is an // optimisation, not a requirement - if the cache is ever cold the worst case is // one extra single-row lookup per batch. if ( $batch_last_id > 0 ) { $batch_last_date = get_post_field( 'post_date', $batch_last_id, 'raw' ); if ( ! empty( $batch_last_date ) ) { $batch_cursor = array( 'date' => $batch_last_date, 'id' => $batch_last_id, ); } } // Keep the fallback paging moving too, so a preview whose query was re-ordered // by a filter still advances over its queries instead of re-reading one window. if ( ! $use_cursor ) { $offset += $last_query_post_count; } // Preview mode: Check if we need to query more products if ( $is_preview_mode ) { // Continue querying if: // 1. We haven't found enough products yet // 2. We haven't exceeded max queries // 3. The last query returned results (not exhausted all products) $should_continue = ( $preview_found_count < $preview_target_count && $preview_query_count < $preview_max_queries && $last_query_post_count > 0 ); } else { $should_continue = false; } } while ( $should_continue ); // Add processed products to array // if(get_option('woosea_duplicates')){ // update_option($channel_duplicates, $prevent_duplicates, 'no'); // } /** * Write row to CSV/TXT or XML or JSONL file */ if ( ( $file_format == 'jsonl' || $file_format == 'jsonl.gz' ) && is_array( $xml_piece ) && ! empty( $xml_piece ) ) { $jsonl_writer = \AdTribes\PFP\Classes\Feed_Writers\Feed_Writer_JSONL::instance(); $file = $jsonl_writer->write_feed( array_filter( $xml_piece ), $feed, false ); unset( $xml_piece ); } elseif ( $file_format != 'xml' && is_array( $products ) && ! empty( $products ) ) { $file = $this->woosea_create_csvtxt_feed( array_filter( $products ), $feed, 'false' ); } else { if ( is_array( $xml_piece ) && $file_format === 'xml' ) { $file = $this->woosea_create_xml_feed( array_filter( $xml_piece ), $feed, 'false' ); unset( $xml_piece ); } unset( $products ); } // Hand the resume point to the next batch of this run. Preview mode pages // entirely within this request, so it keeps its cursor local and never persists // it onto the feed. if ( ! $is_preview_mode ) { $feed->batch_cursor = $batch_cursor; } $feed->save(); /** * Ready creating file, clean up our feed configuration mess now */ delete_option( 'attributes_dropdown' ); delete_option( 'channel_attributes' ); } /** * Calculate the value of an attribute */ public function woosea_calculate_value( $feed, $xml_product ) { $feed_channel = $feed->channel; $feed_attributes = $feed->attributes; /** * Filter the feed attributes before processing. * * @since 13.5.0 * @param array $feed_attributes The feed attributes array. * @return array */ $feed_attributes = apply_filters( 'adt_feed_get_attributes', $feed_attributes ); if ( empty( $feed_channel ) || empty( $feed_attributes ) ) { return $xml_product; } // trim whitespaces from attribute values $xml_product = array_map( 'trim', $xml_product ); // Check if we need to 'calculate' the identifier_exists attribute value. // The helper call below further gates on the feed actually having identifier_exists // mapped to Plugin Calculation, so this runs for any channel that configured it. // Some channels (e.g. Facebook DRM) emit these keys without the g: namespace prefix, // so we accept both naming conventions here. if ( isset( $xml_product['g:condition'] ) && ! isset( $xml_product['g:identifier_exists'] ) && ! isset( $xml_product['identifier_exists'] ) ) { $identifier_exists = 'no'; // default value is no // Per Google's requirements, we only need identifier_exists if the product is new if ( strtolower( $xml_product['g:condition'] ) === 'new' ) { $brand = $xml_product['g:brand'] ?? $xml_product['brand'] ?? ''; $gtin = $xml_product['g:gtin'] ?? $xml_product['gtin'] ?? ''; $mpn = $xml_product['g:mpn'] ?? $xml_product['mpn'] ?? ''; if ( '' !== $brand && ( '' !== $gtin || '' !== $mpn ) ) { $identifier_exists = 'yes'; } } // Only add identifier_exists to the feed if it's configured as a Plugin Calculation, // and use whichever attribute name the channel declares (g:identifier_exists or identifier_exists). $identifier_exists_attribute = $this->get_calculated_identifier_exists_attribute( $feed_attributes ); if ( null !== $identifier_exists_attribute ) { $xml_product[ $identifier_exists_attribute ] = $identifier_exists; } } if ( $feed_channel['name'] == 'Mall.sk' ) { $has_calculated_attribute = false; foreach ($feed_attributes as $attr) { if (isset($attr['mapfrom']) && $attr['mapfrom'] === 'calculated') { $has_calculated_attribute = true; break; } } if ($has_calculated_attribute) { $xml_product['VARIABLE_PARAMS'] = 'calculated'; } } return $xml_product; } /** * Get the configured identifier_exists attribute name when it is mapped to Plugin Calculation. * * Some channels (e.g. Google Shopping) use the 'g:identifier_exists' name while others * (e.g. Facebook DRM) use the bare 'identifier_exists' name. Returns whichever variant is * configured on the feed, or null if no Plugin Calculation mapping exists for it. * * @param array $feed_attributes Feed attribute definitions. * @return string|null The configured attribute name or null when not set to Plugin Calculation. */ private function get_calculated_identifier_exists_attribute( $feed_attributes ) { $identifier_exists_attributes = array( 'g:identifier_exists', 'identifier_exists' ); foreach ( $feed_attributes as $attr ) { if ( ! isset( $attr['attribute'], $attr['mapfrom'] ) ) { continue; } if ( 'calculated' === $attr['mapfrom'] && in_array( $attr['attribute'], $identifier_exists_attributes, true ) ) { return $attr['attribute']; } } return null; } /** * Make start and end sale date readable */ public function get_sale_date( $id, $name ) { $date = $this->get_attribute_value( $id, $name ); if ( $date ) { if ( is_int( $date ) ) { return date( 'Y-m-d', $date ); } } return false; } /** * Get product stock */ public function get_stock( $id ) { $status = $this->get_attribute_value( $id, '_stock_status' ); if ( $status ) { if ( $status == 'instock' ) { return 'in stock'; } elseif ( $status == 'outofstock' ) { return 'out of stock'; } } return 'out of stock'; } /** * Create proper format image URL's */ public function get_image_url( $image_url = '' ) { if ( ! empty( $image_url ) ) { if ( substr( trim( $image_url ), 0, 4 ) === 'http' || substr( trim( $image_url ), 0, 5 ) === 'https' || substr( trim( $image_url ), 0, 3 ) === 'ftp' || substr( trim( $image_url ), 0, 4 ) === 'sftp' ) { return rtrim( $image_url, '/' ); } else { $base = get_site_url(); $image_url = $base . $image_url; return rtrim( $image_url, '/' ); } } return $image_url; } /** * Get attribute value */ public function get_attribute_value( $id, $name ) { if ( str_contains( $name, 'attribute_pa' ) ) { $taxonomy = str_replace( 'attribute_', '', $name ); $meta = get_post_meta( $id, $name, true ); $term = get_term_by( 'slug', $meta, $taxonomy ); return $term->name; } else { return get_post_meta( $id, $name, true ); } } /** * Execute category taxonomy mappings */ private function woocommerce_sea_mappings( $project_mappings, $product_data ) { $original_cat = $product_data['categories']; $original_cat = preg_replace( '/&/', '&', $original_cat ); $original_cat = preg_replace( '/>/', '>', $original_cat ); $original_cat = ltrim( $original_cat, '||' ); $tmp_cat = ''; $match = 'false'; foreach ( $project_mappings as $pm_key => $pm_array ) { // Strip slashes $pm_array['criteria'] = str_replace( '\\', '', $pm_array['criteria'] ); $pm_array['criteria'] = str_replace( '/', '', $pm_array['criteria'] ); $pm_array['criteria'] = trim( $pm_array['criteria'] ); $original_cat = str_replace( '\\', '', $original_cat ); $original_cat = str_replace( '/', '', $original_cat ); $original_cat = trim( $original_cat ); // First check if there is a category mapping for this specific product if ( ( preg_match( '/' . $pm_array['criteria'] . '/', $original_cat ) ) ) { if ( ! empty( $pm_array['map_to_category'] ) ) { $prod_id_cat = $product_data['id']; if ( $product_data['product_type'] == 'variation' ) { $prod_id_cat = $product_data['item_group_id']; } $product_cats_ids = wc_get_product_term_ids( $prod_id_cat, 'product_cat' ); if ( in_array( $pm_array['categoryId'], $product_cats_ids ) ) { $category_pieces = explode( '-', $pm_array['map_to_category'] ); $tmp_cat = $category_pieces[0]; $match = 'true'; } } } elseif ( $pm_array['criteria'] == $original_cat ) { $category_pieces = explode( '-', $pm_array['map_to_category'] ); $tmp_cat = $category_pieces[0]; $match = 'true'; } else { // Do nothing } } if ( $match == 'true' ) { if ( array_key_exists( 'id', $product_data ) ) { $product_data['categories'] = $tmp_cat; } } else { // No mapping found so make google_product_category empty $product_data['categories'] = ''; } return $product_data; } /** * Function to exclude products based on individual product exclusions */ private function woosea_exclude_individual( $product_data ) { $allowed = 1; // Check if product was already excluded from the feed $product_excluded = ucfirst( get_post_meta( $product_data['id'], '_woosea_exclude_product', true ) ); if ( $product_excluded == 'Yes' ) { $allowed = 0; } if ( $allowed < 1 ) { $product_data = array(); $product_data = null; } else { return $product_data; } } /** * Do analysis of product data for Google Shopping */ private function woosea_gs_analysis( $project, $product_data ) { $gs_analysis_check = array(); $gs_analysis_check['project_hash'] = $project['project_hash']; $gs_analysis_check['project_hash']['timestamp'] = 'llqlql'; // Check title criteria $length_title = strlen( $product_data['title'] ); $gs_analysis_check['project_hash'][ $product_data['id'] ]['title']['length'] = $length_title; $gs_analysis_check['project_hash'][ $product_data['id'] ]['title']['more_information'] = 'https://support.google.com/merchants/answer/6324415'; if ( $length_title > 150 ) { $gs_analysis_check[ $product_data['project_hash']['id'] ]['title']['passed_check'] = 'no'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['title']['notification'] = "Your title / product name is too long ($length_title characters), it has been truncated to 150 characters in order to meet Google's criteria."; } elseif ( $length_title < 64 and $length_title > 0 ) { $gs_analysis_check[ $product_data['project_hash']['id'] ]['title']['passed_check'] = 'no'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['title']['notification'] = "Your title / product name is too short ($length_title characters), make sure it is over 64 characters long. Best practice is to use all 150 characters. Include the important details that define your product."; } elseif ( $length_title < 1 ) { $gs_analysis_check[ $product_data['project_hash']['id'] ]['title']['passed_check'] = 'no'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['title']['notification'] = 'Your title / product name is empty, make sure it is over 64 characters long. Best practice is to use all 150 characters. Include the important details that define your product.'; } else { $gs_analysis_check[ $product_data['project_hash']['id'] ]['title']['passed_check'] = 'yes'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['title']['notification'] = 'The length of your title / product name is perfect, well done!'; } // Check description criteria $length_description = strlen( $product_data['description'] ); $gs_analysis_check[ $product_data['project_hash']['id'] ]['description']['length'] = $length_description; $gs_analysis_check[ $product_data['project_hash']['id'] ]['description']['more_information'] = 'https://support.google.com/merchants/answer/6324468'; if ( $length_description > 5000 ) { $gs_analysis_check[ $product_data['project_hash']['id'] ]['description']['passed_check'] = 'no'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['description']['notification'] = "Your product description is too long ($length_description characters), make sure your product description is no longer than 5000 characters."; } elseif ( $length_description < 160 and $length_description > 0 ) { $gs_analysis_check[ $product_data['project_hash']['id'] ]['description']['passed_check'] = 'no'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['description']['notification'] = "Your product description is too short ($length_description characters), make sure to list the most important details in the first 160 - 500 characters."; } elseif ( $length_description < 1 ) { $gs_analysis_check[ $product_data['project_hash']['id'] ]['description']['passed_check'] = 'no'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['description']['notification'] = 'Your product description is empty, make sure to list the most important details in the first 160 - 500 characters.'; } else { $gs_analysis_check[ $product_data['project_hash']['id'] ]['description']['passed_check'] = 'yes'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['description']['notification'] = 'The length of your title / product name is perfect, well done!'; } // Check availability $gs_analysis_check[ $product_data['project_hash']['id'] ]['availability']['more_information'] = 'https://support.google.com/merchants/answer/6324448'; $availability_allowed = array( 'in_stock', 'out_of_stock', 'preorder', 'backorder' ); $availability = $product_data['availability']; if ( ! in_array( $product_data['availability'], $availability_allowed ) ) { $gs_analysis_check[ $product_data['project_hash']['id'] ]['availability']['passed_check'] = 'no'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['availability']['notification'] = "Your availability value ($availability) does not meet Google's requirements (make sure the value is in_stock, out_of_stock, preorder or backorder)."; } else { $gs_analysis_check[ $product_data['project_hash']['id'] ]['availability']['passed_check'] = 'yes'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['availability']['notification'] = "Your availability value ($availability) meets Google's requirements, well done!"; } // Check link $length_link = strlen( $product_data['link'] ); $gs_analysis_check[ $product_data['project_hash']['id'] ]['link']['length'] = $length_link; $gs_analysis_check[ $product_data['project_hash']['id'] ]['link']['more_information'] = 'https://support.google.com/merchants/answer/6324416'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['link']['passed_check'] = 'yes'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['link']['notification'] = "Your product link (URL) meets Google's requirements."; if ( $length_link > 2000 ) { $gs_analysis_check[ $product_data['project_hash']['id'] ]['link']['passed_check'] = 'no'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['link']['notification'] = "Your product link (URL) is too long ($length_link characters), make sure the product link (URL) is no longer than 2000 characters."; } elseif ( $length_link < 1 ) { $gs_analysis_check[ $product_data['project_hash']['id'] ]['link']['passed_check'] = 'no'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['link']['notification'] = 'Your product link (URL) is empty.'; } else { } if ( ! filter_var( $product_data['link'], FILTER_VALIDATE_URL ) !== false ) { $gs_analysis_check[ $product_data['project_hash']['id'] ]['link']['passed_check'] = 'no'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['link']['notification'] = "Your product link (URL) doesn't appear to be a valid URL."; } $url = parse_url( $product_data['link'] ); $allowed_schema = array( 'http', 'https' ); if ( ! in_array( $url['scheme'], $allowed_schema ) ) { $gs_analysis_check[ $product_data['project_hash']['id'] ]['link']['passed_check'] = 'no'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['link']['notification'] = "Your product link (URL) doesn't http or https, which is required by Google."; } // Check price $length_price = strlen( $product_data['price'] ); $product_price = $product_data['price']; $gs_analysis_check[ $product_data['project_hash']['id'] ]['price']['length'] = $length_price; $gs_analysis_check[ $product_data['project_hash']['id'] ]['price']['more_information'] = 'https://support.google.com/merchants/answer/6324371'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['price']['passed_check'] = 'yes'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['price']['notification'] = "Your product price ($product_price) meets Google's requirements."; if ( $length_price < 1 ) { $gs_analysis_check[ $product_data['project_hash']['id'] ]['price']['passed_check'] = 'no'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['price']['notification'] = 'Your product price is empty, make sure to add a price to all your products.'; } $separator = wc_get_price_decimal_separator(); if ( $separator == '.' ) { if ( ! is_numeric( $product_data['price'] ) ) { $gs_analysis_check[ $product_data['project_hash']['id'] ]['price']['passed_check'] = 'no'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['price']['notification'] = "Your product price ($product_price) doesn't appear to be a valid price."; } } if ( empty( $product_data['price'] ) ) { $gs_analysis_check[ $product_data['project_hash']['id'] ]['price']['passed_check'] = 'no'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['price']['notification'] = "Your product price ($product_price) doesn't appear to be a valid price, it cannot be empty of 0 (zero)"; } if ( $product_data['price'] == '0,00' ) { $gs_analysis_check[ $product_data['project_hash']['id'] ]['price']['passed_check'] = 'no'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['price']['notification'] = "Your product price ($product_price) doesn't appear to be a valid price, it cannot be empty of 0 (zero)"; } // Product type $length_product_type = strlen( $product_data['product_type'] ); $product_type = $product_data['product_type']; $gs_analysis_check[ $product_data['project_hash']['id'] ]['product_type']['length'] = $length_price; $gs_analysis_check[ $product_data['project_hash']['id'] ]['product_type']['more_information'] = 'https://support.google.com/merchants/answer/6324406'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['product_type']['passed_check'] = 'yes'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['product_type']['notification'] = "Your product type meets Google's requirements."; if ( $length_product_type > 750 ) { $gs_analysis_check[ $product_data['project_hash']['id'] ]['product_type']['passed_check'] = 'no'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['product_type']['notification'] = 'Your product type value is too long, make sure it is no longer than 750 characters.'; } elseif ( $length_product_type < 1 ) { $gs_analysis_check[ $product_data['project_hash']['id'] ]['product_type']['passed_check'] = 'no'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['product_type']['notification'] = 'Your product type field is empty.'; } else { } // Condition $gs_analysis_check[ $product_data['project_hash']['id'] ]['condition']['more_information'] = 'https://support.google.com/merchants/answer/6324469'; $condition_allowed = array( 'new', 'refurbished', 'used', 'New', 'Refurbished', 'Used' ); $condition = $product_data['condition']; if ( ! in_array( $product_data['condition'], $condition_allowed ) ) { $gs_analysis_check[ $product_data['project_hash']['id'] ]['condition']['passed_check'] = 'no'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['condition']['notification'] = "Your condition value ($condition) does not meet Google's requirements (make sure the value is new, refurbished or used)."; } else { $gs_analysis_check[ $product_data['project_hash']['id'] ]['condition']['passed_check'] = 'yes'; $gs_analysis_check[ $product_data['project_hash']['id'] ]['condition']['notification'] = "Your condition value ($condition) meets Google's requirements, well done!"; } // Save Google Shopping analysis results update_option( 'woosea_gs_analysis_results', $gs_analysis_check, false ); } /** * Get comma separated categories * * @param array $category_ids The category IDs. * @return string The comma separated categories. */ private function get_comma_separated_categories( $category_ids ) { $categories_comma_separated = array(); // Build category hierarchy map $categories_by_parent = array(); $all_categories = array(); foreach ( $category_ids as $category_id ) { $category = get_term( $category_id, 'product_cat' ); if ( ! is_wp_error( $category ) ) { $all_categories[ $category_id ] = $category; $parent_id = $category->parent; if ( ! isset( $categories_by_parent[ $parent_id ] ) ) { $categories_by_parent[ $parent_id ] = array(); } $categories_by_parent[ $parent_id ][] = $category_id; } } // Recursive function to add categories in hierarchical order $add_categories_hierarchically = function( $parent_id, $categories_by_parent, $all_categories, &$result ) use ( &$add_categories_hierarchically ) { if ( ! isset( $categories_by_parent[ $parent_id ] ) ) { return; } foreach ( $categories_by_parent[ $parent_id ] as $category_id ) { if ( isset( $all_categories[ $category_id ] ) ) { $result[] = $all_categories[ $category_id ]->name; // Recursively add children of this category $add_categories_hierarchically( $category_id, $categories_by_parent, $all_categories, $result ); } } }; // Start with root categories (parent = 0) and build hierarchically $add_categories_hierarchically( 0, $categories_by_parent, $all_categories, $categories_comma_separated ); return implode( ',', $categories_comma_separated ); } } ion of %1$s, please activate your subscription in %2$sMyYoast%3$s and update to the latest version (at least 17.7) to gain access to our updated workouts section.'=>'נראה שאתה מפעיל גרסה מיושנת שאיננה מופעלת של %1$s, אנא הפעל את המנוי שלך ב-%2$sMyYoast%3$s ועדכן לגרסה העדכנית ביותר (לפחות 17.7) כדי לקבל גישה למדור האימונים המעודכן שלנו.','Activate your subscription of %s'=>'הפעל את מנוי הפרו עבור- %s','It looks like you\'re running an outdated version of %1$s, please %2$supdate to the latest version (at least 17.7)%3$s to gain access to our updated workouts section.'=>'נראה שאתה מפעיל גרסה מיושנת של %1$s, אנא %2$s עדכן לגרסה העדכנית ביותר (לפחות 17.7) %3$s כדי לקבל גישה למדור האימונים המעודכן שלנו.','Update to the latest version of %s'=>'עדכן לגרסה העדכנית ביותר של %s','Renew your subscription'=>'חדש את המנוי שלך','Accessing the latest workouts requires an updated version of %s (at least 17.7), but it looks like your subscription has expired. Please renew your subscription to update and gain access to all the latest features.'=>'גישה לאימונים האחרונים דורשת גרסה מעודכנת של %s (לפחות 17.7), אבל נראה שהמינוי שלך פג וכבר לא בתוקף. אנא חדש את המנוי שלך על מנת לעדכן את התוסף ולקבל גישה לכל האפשרויות העדכניות ביותר.','Renew your subscription of %s'=>'עדכן את המנוי של %s','Get started quickly with the %1$s%2$s First-time configuration%3$s and configure Yoast SEO with the optimal SEO settings for your site!'=>'התחלת עבודה מהירה באמצעות הגדרת %1$s%2$s אימון התצורה%3$s והגדר את Yoast SEO להגדרות ה-SEO האופטימליות עבור האתר שלך!','Current or first category title'=>'כותרת קטגוריה נוכחית או ראשונה','Category Title'=>'כותרת הקטגוריה','Replaced with the post content'=>'החלף עם התוכן של הפוסט','Post Content'=>'תוכן הפוסט','Replaced with the permalink'=>'החלף עם המבנה קישור','Replaced with the last name of the author'=>'החלף עם שם המשפחה של הסופר','Author last name'=>'שם משפחה של הסופר','Replaced with the first name of the author'=>'החלף שם פרטי של הסופר','Author first name'=>'שם פרטי של הסופר','Replaced with the day the post was published'=>'החלף יום פרסום הפוסט','Post day'=>'תאריך הפוסט','Replaced with the month the post was published'=>'החלף חודש פירסום הפוסט','Post month'=>'חודש הפוסט','Replaced with the year the post was published'=>'החלף שנת פירסום הפוסט','Post year'=>'שנת הפוסט','Current day'=>'יום נוכחי','Current month'=>'חודש נוכחי','Current date'=>'תאריך נוכחי','It looks like you aren\'t using our %1$s%2$s addon%3$s. %4$sUpgrade today%5$s to unlock more tools and SEO features to make your products stand out in search results.'=>'נראה שאתה לא משתמש בתוסף %1$s%2$s%3$s שלנו. %4$sשדרג היום%5$s כדי לקבל גישה לעוד כלים ואפשרויות לקידום אתרים כדי להבליט את המוצרים שלך בתוצאות החיפוש.','Below are the technical details for the error. See %1$sthis page%2$s for a more detailed explanation.'=>'להלן הפרטים הטכניים עבור השגיאה. ראה %1$sדף זה%2$s להסבר מפורט יותר.','Workouts'=>'תרגולים','Improve the quality of your site search! Automatically helps your users find your cornerstone and most important content in your internal search results. It also removes noindexed posts & pages from your site’s search results.'=>'שפר את איכות החיפוש באתר שלך! עוזר אוטומטית למשתמשים שלך למצוא את אבן הפינה שלך ואת התוכן החשוב ביותר בתוצאות החיפוש הפנימיות שלך. זה גם מסיר פוסטים ודפים ללא אינדוקס מתוצאות החיפוש של האתר שלך.','Find out more about our %s integration.'=>'מידע נוסף על הרחבת %s שלנו.','Read more about how internal linking can improve your site structure.'=>'קרא עוד על האופן שבו קישור פנימי יכול לשפר את מבנה האתר שלך.','Get relevant internal linking suggestions — while you’re writing! The link suggestions metabox shows a list of posts on your blog with similar content that might be interesting to link to. '=>'קבל הצעות רלוונטיות לקישור פנימי - בזמן שאתה כותב! המטא-בוקס של הצעות קישור מציג רשימה של פוסטים בבלוג שלך עם תוכן דומה שעשוי להיות מעניין לקשר אליו. ','Link suggestions'=>'הצעות לקישורים','Find out how Insights can help you improve your content.'=>'גלה כיצד תובנות יכולות לעזור לך לשפר את התוכן שלך.','Find relevant data about your content right in the Insights section in the Yoast SEO metabox. You’ll see what words you use most often and if they’re a match with your keywords! '=>'מצא נתונים רלוונטיים על התוכן שלך ישירות באזור \'תובנות\' במטא-בוקס של Yoast SEO. תראה באילו מילים אתה משתמש בתדירות הגבוהה ביותר ואם הן מתאימות למילות המפתח שלך! ','Insights'=>'תובנות','Oops, something has gone wrong and we couldn\'t complete the optimization of your SEO data. Please make sure to activate your subscription in MyYoast by completing %1$sthese steps%2$s.'=>'אופס, משהו השתבש ולא הצלחנו להשלים את האופטימיזציה של נתוני ה-SEO שלך. אנא הקפד להפעיל את המנוי שלך ב-MyYoast על ידי השלמת %1$sשלבים%2$s אלה.','No %1$s plugins have been installed. You don\'t seem to own any active subscriptions.'=>'לא הותקנו תוספים של %1$s. נראה שאין לך מנויים פעילים כלשהם.','Addon installation failed because of an error: %s.'=>'התקנת התוסף נכשלה בגלל שגיאה: %s.','You are not allowed to install plugins.'=>'אינך רשאי להתקין תוספים.','Addon installed.'=>'תוסף מותקן.','Addon activation failed because of an error: %s.'=>'הפעלת התוסף נכשלה בגלל שגיאה: %s.','You are not allowed to activate plugins.'=>'אינך רשאי להפעיל תוספים.','Addon activated.'=>'תוסף הופעל.','%1$s Continue to %2$s%3$s'=>'%1$s המשך אל %2$s%3$s','Installing and activating addons'=>'מתקין ומפעיל את התוספים','Required by %s'=>'נדרש על ידי %s','Auto-updates are disabled based on this setting for %1$s.'=>'עדכונים אוטומטיים מושבתים על סמך הגדרה זו עבור %1$s.','Auto-updates are enabled based on this setting for %1$s.'=>'עדכונים אוטומטיים מופעלים על סמך הגדרה זו עבור %1$s.','New'=>'חדש','The post could not be found.'=>'לא ניתן למצוא את הפוסט.','The term is considered invalid. The following reason was given by WordPress: %s'=>'המונח נחשב כפסול. סיבה של וורדפרס לכך היא: %s','The term could not be found.'=>'לא ניתן למצוא את המונח.','This feature has been disabled since subsites never send tracking data.'=>'אפשרות זו הושבתה מכיוון שאתרי משנה לעולם לא שולחים נתוני מעקב.','We notice that you have installed WPML. To make sure your canonical URLs are set correctly, %1$sinstall and activate the WPML SEO add-on%2$s as well!'=>'שמנו לב שהתקנת WPML. כדי לוודא שכתובות האתרים הקנוניות מוגדרות כהלכה, %1$sיש להתקין ולהפעיל גם את ההרחבה WPML SEO%2$s!','Because of a change in your category base setting, some of your SEO data needs to be reprocessed.'=>'בגלל שינוי במבנה הקישורים של הקטגוריות, צריך לעבד מחדש חלק מנתוני ה-SEO.','Find out how a rich snippet can improve visibility and click-through-rate.'=>'גלו כיצד קטע טקסט עשיר יכול לשפר את הנראות ושיעור ההקלקות.','This adds an author byline and reading time estimate to the article’s snippet when shared on Slack.'=>'מוסיף את שם המחבר ואומדן זמן קריאה כאשר המאמר משותף בסלאק.','Enhanced Slack sharing'=>'שיתוף סלאק משופר','Wait for a week or so, until %1$s automatically processes most of your content in the background.'=>'המתן כשבוע עד ש-%1$s יעבד אוטומטית את רוב התוכן ברקע.','%s minute'=>'דקה %s' . "\0" . '%s דקות','Est. reading time'=>'זמן קריאה מוערך','Written by'=>'נכתב על יד','Google Rich Results Test'=>'בדיקת תוצאות עשירות בגוגל','Because of a change in your tag base setting, some of your SEO data needs to be reprocessed.'=>'בגלל שינוי שנעשה במבנה קישורי התגיות, יש לעבד מחדש חלק מנתוני ה- SEO שלך. ','The %s integration offers suggestions and insights for keywords related to the entered focus keyphrase.'=>'הרחבת %s מציעה הצעות ותובנות עבור מילות מפתח הקשורות לביטוי המיקוד שהוזן.','This tab allows you to selectively disable %1$s integrations with third-party products for all sites in the network. By default all integrations are enabled, which allows site admins to choose for themselves if they want to toggle an integration on or off for their site. When you disable an integration here, site admins will not be able to use that integration at all.'=>'כרטיסייה זו מאפשרת להשבית באופן סלקטיבי הרחבות של %1$s עם צדדים שלישיים עבור כל האתרים ברשת. כברירת מחדל כל ההרחבות מופעלות, מה שמאפשר למנהלי האתרים לבחור בעצמם אם הם רוצים להפעיל או לכבות הרחבה מסויימת לאתר שלהם. כאשר הרחבה מסויימת מושבתת כאן, מנהלי האתרים כלל לא יוכלו להשתמש בהרחבה זו.','Activate your subscription'=>'הפעל את המנוי שלך','Oops, something has gone wrong and we couldn\'t complete the optimization of your SEO data. Please click the button again to re-start the process. '=>'אופס, משהו השתבש ולא יכולנו להשלים את האופטימיזציה של הנתונים שלך. לחץ על הכפתור כדי להתחיל את התהליך מחדש.','All permalinks were successfully reset'=>'כל מבני הקישורים אופסו בהצלחה','You can speed up your site and get insight into your internal linking structure by letting us perform a few optimizations to the way SEO data is stored.'=>'ניתן להאיץ את האתר ולקבל תובנות על מבנה הקישור הפנימי על ידי מתן אישור לביצוע מספר אופטימיזציות לאופן שמירת נתוני ה-SEO.','Start SEO data optimization'=>'התחל אופטימיזציית נתונים','Learn more about the benefits of optimized SEO data.'=>'למד עוד על הייתרונות באופטימזציית הנתונים.','You can speed up your site and get insight into your internal linking structure by letting us perform a few optimizations to the way SEO data is stored. If you have a lot of content it might take a while, but trust us, it\'s worth it.'=>'ניתן להאיץ את האתר ולקבל תובנות על מבנה הקישור הפנימי על ידי מתן אישור לביצוע מספר אופטימיזציות לאופן שמירת נתוני ה-SEO. אם יש באתר הרבה תוכן הדבר עלול לקחת זמן, אך סמוך עלינו, זה שווה את זה.','Optimize SEO Data'=>'אופטימיזציה לנתוני SEO','If the problem persists, please contact support.'=>'אם הבעיה נמשכת, ליצור קשר עם התמיכה.','Something has gone wrong and we couldn\'t complete the optimization of your SEO data. Please %1$sre-start the process%2$s.'=>'משהו השתבש ולא הצלחנו להשלים את האופטימיזציה לנתוני SEO. נא %1$s להתחיל שוב את התהליך %2$s.','If you still need support and have an active subscription for this product, please email %s.'=>'אם אתה עדיין זקוק לתמיכה ויש לך מנוי פעיל למוצר זה, אפשר לשלוח אימייל אל %s.','You can probably find an answer to your question in our %1$shelp center%2$s.'=>'כנראה שתמצא תשובה לשאלתך ב%1$sמרכז התמיכה%2$s.','Need support?'=>'צריך תמיכה?','Disabling Yoast SEO\'s XML sitemaps will not disable WordPress\' core sitemaps. In some cases, this %1$s may result in SEO errors on your site%2$s. These may be reported in Google Search Console and other tools.'=>'כיבוי מפות האתר ​​של Yoast SEO לא תשבית את מפות האתר של וורדפרס. במקרים מסוימים, %1$s עלול לגרום לשגיאות SEO באתר %2$s. ניתן לדווח עליהם ב- Google Search Console ובכלים אחרים.','The advanced section of the %1$s meta box allows a user to remove posts from the search results or change the canonical. The settings in the schema tab allows a user to change schema meta data for a post. These are things you might not want any author to do. That\'s why, by default, only editors and administrators can do this. Setting to "%2$s" allows all users to change these settings.'=>'בהגדרות המתקדמות בתיבת המטא %1$s יכול למשתמש להסיר פוסטים מתוצאות החיפוש או לשנות את הכתובת הקנונית. ההגדרות בלשונית הסכימה מאפשרות למשתמש לשנות מטא נתונים של סכימות עבור פוסט. סביר להניח שלא תרצו שאף מחבר יוכל לשנות את ההגדרות, ולכן, כברירת מחדל, רק עורכים ומנהלי יכולים לעשות זאת. על מנת לאפשר לכל המשתמשים לשנות את הגדרות מטא הנתונים יש להגדיר "%2$s" ','Security: no advanced or schema settings for authors'=>'אבטחה: אין הגדרות מתקדמות או סכימה עבור מחברים','Report'=>'דוח','Tech Article'=>'מאמר טכנולוגי','Scholarly Article'=>'מאמר אקדמי','Satirical Article'=>'מאמר סאטירי','Advertiser Content Article'=>'מאמר עם תוכן שיווקי','News Article'=>'מאמר חדשותי','Social Media Posting'=>'פרסום ברשתות חברתיות','Article'=>'מאמר','Search Results Page'=>'עמוד תוצאות חיפוש','Real Estate Listing'=>'רישומי נדל"ן','Checkout Page'=>'עמוד תשלום','Collection Page'=>'עמוד אוסף','Medical Web Page'=>'עמוד רפואי','Contact Page'=>'עמוד יצירת קשר','Profile Page'=>'עמוד פרופיל','QA Page'=>'עמוד שאלות תשובות','FAQ Page'=>'עמוד שאלות נפוצות','About Page'=>'עמוד אודות','Item Page'=>'עמוד פריט','Web Page'=>'עמוד אינטרנט','Allow us to track some data about your site to improve our plugin.'=>'אפשר לנו לעקוב אחר נתונים באתר שלך כדי לשפר את התוסף שלנו.','Usage tracking'=>'מעקב שימוש','Because of a change in your permalink structure, some of your SEO data needs to be reprocessed.'=>'בגלל שינוי במבנה הקישורים באתר, צריך לעבד מחדש חלק מנתוני ה-SEO.','Because of a change in your home URL setting, some of your SEO data needs to be reprocessed.'=>'בגלל שינוי במבנה הקישורים של העמוד הראשי, צריך לעבד מחדש חלק מנתוני ה-SEO.','%1$s Internal Linking Blocks'=>'בלוק קישור פנימיים של %1$s','%1$sFind out how to solve this problem on our help center%2$s.'=>'%1$sגלה כיצד לפתור בעיה זו במרכז התמיכה שלנו%2$s.','The text link counter feature is not working as expected'=>'מונה הקישורים אינו פועל כמצופה','The text link counter helps you improve your site structure. %1$sFind out how the text link counter can enhance your SEO%2$s.'=>'מונה קישור עוזר לשפר את מבנה האתר. %1$s גלו כיצד מונה הקישורים יכול לשפר את ה-SEO%2$s.','The text link counter is working as expected'=>'מונה הקישור פועל כמצופה','The links columns show the number of articles on this site linking %3$sto%4$s this article and the number of URLs linked %3$sfrom%4$s this article. Learn more about %1$show to use these features to improve your internal linking%2$s, which greatly enhances your SEO.'=>'עמודות הקישורים מציגות את מספר המאמרים באתר זה המקשרים %3$sאל%4$s המאמר הזה ומספר הקישורים המקושרים %3$sמתוך%4$s המאמר הזה. למידע נוסף על %1$sהצגת שימוש באפשרויות אלה לשיפור הקישורים הפנימיים%2$s, מה שמשפר מאוד את ה-SEO.','We\'ve written an article about %1$show to use the SEO score and Readability score%2$s.'=>'כתבנו מאמר על %1$s איך להשתמש בציון SEO וציון קריאוּת %2$s.','%1$s adds several columns to this page.'=>'%1$s מוסיף מספר עמודות לעמוד הזה.','I don\'t want this site to show in the search results.'=>'אני לא רוצה שהאתר יופיע בתוצאות חיפוש.','If you want search engines to show this site in their results, you must %1$sgo to your Reading Settings%2$s and uncheck the box for Search Engine Visibility.'=>'אם ברצונך לאפשר למנועי חיפוש להציג אתר זה בתוצאותיהם, יש %1$sלעבור להגדרות הקריאה%2$s ולבטל את הסימון בתיבה הנראות במנוע חיפוש.','Show debug information'=>'הצג מידע לניפוי תקלות','Your site will continue to work normally, but won\'t take full advantage of %s.'=>'האתר ימשיך לעבוד כרגיל, אך לא ינצל את מלוא היתרונות של %s.','%s had problems creating the database tables needed to speed up your site.'=>'%s נתקל בבעיות ביצירת טבלאות במסד הנתונים הדרושות על מנת להאיץ את האתר.',' We estimate this will take less than a minute.'=>' אנו מעריכים שזה יארך פחות מדקה.',' We estimate this will take a couple of minutes.'=>' אנו מעריכים שזה יארך מספר דקות.','%1$sRun the indexation process on your server%2$s using %3$sWP CLI%2$s.'=>'%1$s הפעל את תהליך האינדוקס בשרת %2$s באמצעות %3$sWP CLI%2$s',' We estimate this could take a long time, due to the size of your site. As an alternative to waiting, you could:'=>' אנו מעריכים שבגלל גודל האתר, הדבר עשוי לארוך זמן רב. כחלופה להמתנה, ניתן:','Please read %1$sthis help article%2$s to find out how to resolve this problem.'=>'מומלץ לקרוא %1$s מאמר העזרה זה %2$s כדי לברר כיצד לפתור בעיה זו.','Replaced with the term ancestors hierarchy'=>'הוחלף במונח ההיררכיה של ההורה','Term hierarchy'=>'היררכיית מונחים','This %1$s REST API endpoint gives you all the metadata you need for a specific URL. This will make it very easy for headless WordPress sites to use %1$s for all their SEO meta output.'=>'נקודת קצה של REST API עבור %1$s מציגות את כל נתוני המטא שצריך לכתובת ספציפית. הדבר יקל מאוד על אתרי וורדפרס להשתמש ב-%1$s עבור כל נתוני המטא SEO שלהם.','REST API: Head endpoint'=>'REST API: נקודת קצה ראשית','%1$sYou can change the tagline in the customizer%2$s.'=>'%1$sניתן לשנות את תיאור האתר במסך ההתאמה האישית%2$s.','You still have the default WordPress tagline. Even an empty one is probably better.'=>'האתר עדיין משתמש בתיאור אתר ברירת המחדל של וורדפרס. אפילו תיאור ריק כנראה יהיה עדיף.','You should change the default WordPress tagline'=>'יש לשנות את תיאור האתר המוגדר כברירת המחדל על ידי וורדפרס','You are using a custom tagline or an empty one.'=>'יש להשתמש בתיאור אתר מותאם או להשאירו ריק.','You changed the default WordPress tagline'=>'שינית את תיאור ברירת המחדל של וורדפרס','Comments on your posts are displayed on a single page. This is just like we\'d suggest it. You\'re doing well!'=>'תגובות לפוסטים מוצגות בעמוד בודד. זה בדיוק כמו שאנחנו מציעים. אתה עושה עבודה טובה!','It\'s highly recommended to have your postname in the URL of your posts and pages. Consider setting your permalink structure to %s.'=>'מומלץ מאוד להכניס את כותרת הפוסט בכתובת ה-URL של הפוסט. יש לשקול להגדיר את מבנה הקישורים של האתר ל-%s.','You do not have your postname in the URL of your posts and pages'=>'שם הפוסט אינו מופיע בכתובת של הפוסטים והעמודים','You do have your postname in the URL of your posts and pages.'=>'שם הפוסט מופיע בכתובת של הפוסטים והעמודים','Your permalink structure includes the post name'=>'מבנה הקישורים מכיל את שם הפוסט','Comments on your posts break into multiple pages. As this is not needed in 999 out of 1000 cases, we recommend you disable it. To fix this, uncheck "Break comments into pages..." on the Discussion Settings page.'=>'תגובות לפוסטים מחולקות למספר עמודים. מכיוון שהדבר לא נדרש ב-999 מתוך 1000 מקרים, אנו ממליצים להשבית אפשרות זו. כדי לתקן זאת, יש להסיר את הסימון של "חלק תגובות לעמודים..." בהגדרות הדיון.','Comments break into multiple pages'=>'הערות נחתכות למספר דפים','Comments are displayed on a single page'=>'תגובות מוצגות בדף יחיד','No title'=>'ללא כותרת','%1$sThis was reported by the %2$s plugin%3$s'=>'%1$s דווח יל ידי התוסף %2$s %3$s','%1$sGo to the Discussion Settings page%2$s'=>'%1$sעבור אל הגדרות דיון%2$s','If you want to apply advanced meta robots settings for this page, please define them in the following field.'=>'אם ברצונך להחיל הגדרות meta רובוטים מתקדמות עבור עמוד זה, יש להגדיר אותן בשדה הבא.','The browser you are currently using is unfortunately rather dated. Since we strive to give you the best experience possible, we no longer support this browser. Instead, please use %1$sFirefox%4$s, %2$sChrome%4$s or %3$sMicrosoft Edge%4$s.'=>'למרבה הצער, הדפדפן בו אתה משתמש כרגע ישן למדי. מכיוון שאנו שואפים להעניק לך את החוויה הטובה ביותר האפשרית, איננו תומכים יותר בדפדפן זה. במקום זאת, אנא השתמש ב-%1$sפיירפוקס%2$s, %4$sכרום%4$s או %3$sמיקרוסופט אדג\'%4$s.','Check out %1$s'=>'תבדקו את %1$s','We have both free and premium online courses to learn everything you need to know about SEO.'=>'יש לנו קורסים מקוונים בחינם ובתשלום ללימוד כל מה שצריך לדעת על קידום אתרים.','Want to learn SEO from Team Yoast? Check out our %1$s!'=>'רוצה ללמוד SEO מצוות Yoast? בדוק את %1$s שלנו!','Learn SEO'=>'לימודי SEO','%s settings to import:'=>'הגדרות %s לייבוא:','Import settings from another %1$s installation by pasting them here and clicking "%2$s".'=>'ייבוא הגדרות מהתקנה אחרת של %1$s על ידי הדבקתן כאן ולחיצה על "%2$s".','Your %1$s settings:'=>'הגדרות %1$s:','Schema'=>'סכימה','Settings saved.'=>'ההגדרות נשמרו.','Show this item.'=>'הצג פריט זה.','Hide this item.'=>'הסתר פריט זה.','You have %d hidden notification:'=>'קיימת התראה מוסתרת %d:' . "\0" . 'קיימות %d התראות מוסתרות:','Focus keyphrase not set'=>'ביטוי מפתח לא הוגדר','The form contains %1$s error. %2$s'=>'הטופס מכיל שגיאה %1$s. %2$s' . "\0" . 'הטופס מכיל %1$s שגיאות. %2$s','Posts with the SEO score: %s'=>'פוסטים עם ציון SEO: %s','%s video tutorial'=>'דרכת וידאו של %s','Post Noindexed'=>'הפוסט Noindexed','No Focus Keyphrase'=>'ללא ביטויי מפתח','SEO: %s'=>'SEO: %s','To view your current crawl errors, %1$splease visit Google Search Console%2$s.'=>'כדי להציג את שגיאות הסריקה הנוכחיות, %1$sיש לבקר בקונסולת החיפוש של גוגל%2$s.','Google has discontinued its Crawl Errors API. Therefore, any possible crawl errors you might have cannot be displayed here anymore. %1$sRead our statement on this for further information%2$s.'=>'גוגל הפסיקה לתמוך ב-API של שגיאות סריקה שלה. לכן, לא ניתן להציג יותר שגיאות סריקה אפשריות. %1$sלמידע נוסף ניתן לקרוא את ההצהרה שלנו בנושא זה%2$s.','Organization'=>'ארגון','You have previously set your site to represent a person. We’ve improved our functionality around Schema and the Knowledge Graph, so you should go in and %1$scomplete those settings%2$s.'=>'בעבר האתר הוגדר לייצוג אדם. שיפרנו את פונקציונליות ה-Schema וגרף הידע, לכן יש %1$sלהשלים הגדרות נוספות%2$s.','(if one exists)'=>'(אם קיים)','Wikipedia page about you'=>'עמוד ויקיפדיה אודותייך','YouTube profile URL'=>'כתובת פרופיל יוטיוב','Tumblr profile URL'=>'כתובת פרופיל טמבלר','SoundCloud profile URL'=>'כתובת פרופיל סאונד-קלאוד','MySpace profile URL'=>'כתובת פרופיל מיי-ספייס','Uncategorized'=>'ללא קטגוריה','Pinterest profile URL'=>'כתובת פרופיל פינטרסט','LinkedIn profile URL'=>'כתובת פרופיל לינקדאין','Instagram profile URL'=>'כתובת פרופיל אינסטגרם','No JSON object was returned.'=>'לא הוחזר אובייקט JSON.','Received internal links'=>'קישורים פנימיים שהתקבלו','Outgoing internal links'=>'קישורים פנימיים יוצאים','Keyphrase'=>'ביטוי מפתח','For this feature to work, %1$s needs to create a table in your database. We were unable to create this table automatically.'=>'כדי שאפשרות זו תעבוד, %1$s צריך ליצור טבלה במסד הנתונים. לא הצלחנו ליצור טבלה זו באופן אוטומטי.','Cannot get the size of %1$s because of unknown reasons.'=>'לא ניתן לקבל את הגודל של %1$s מסיבות לא ידועות.','Cannot get the size of %1$s because it is hosted externally.'=>'לא ניתן לקבל את הגודל של %1$s מכיוון שהוא מתארח בשרת חיצוני.','Page %s'=>'עמוד %s','Method %1$s() does not exist in class %2$s'=>'המתודה %1$s() אינה קיימת בקלאס %2$s','With %s, you can easily create such redirects.'=>'באמצעות %s ניתן ליצור בקלות הפניות כאלה.','Import of settings is only supported on servers that run PHP 5.3 or higher.'=>'ייבוא ​​הגדרות נתמך רק בשרתים שמפעילים PHP 5.3 ומעלה.','Export your %1$s settings here, to copy them on another site.'=>'ייצוא הגדרות %1$s כדי להעתיק אותם באתר אחר.','No settings found.'=>'לא נמצאו הגדרות.','These are settings for the %1$s plugin by %2$s'=>'אלו הן ההגדרות עבור התוסף %1$s מאת %2$s','Copy all these settings to another site\'s %1$s tab and click "%1$s" there.'=>'העתיקו את כל ההגדרות האלה לכרטיסיית %1$s באתר אחר וליחצו שם על "%1$s".','You do not have the required rights to export settings.'=>'אין לך את ההרשאות הנדרשות כדי לייצא נתונים','Upgrade to %s'=>'שדרג אל %s','Learn about why permalinks are important for SEO.'=>'למד מדוע קישורים חשובים לקידום אתרים.','Changing your permalinks settings can seriously impact your search engine visibility. It should almost %1$s never %2$s be done on a live website.'=>'שינוי מבנה קישורים יכול להשפיע משמעותית על נראות האתר במנועי חיפוש. מומלץ %1$s לעולם %2$s לא לבצע את זה על אתר חי.','WARNING:'=>'אזהרה:','Disable'=>'כיבוי','Allow Control'=>'אפשר שליטה','This tab allows you to selectively disable %s features for all sites in the network. By default all features are enabled, which allows site admins to choose for themselves if they want to toggle a feature on or off for their site. When you disable a feature here, site admins will not be able to use that feature at all.'=>'כרטיסייה זו מאפשרת להשבית באופן סלקטיבי אפשרויות של %s עבור כל האתרים ברשת. כברירת מחדל כל האפשרויות מופעלות, מה שמאפשר למנהלי האתרים לבחור בעצמם אם הם רוצים להפעיל או לכבות אפשרות מסויימת לאתר שלהם. כאשר אפשרות מסוימת מושבתת כאן, מנהלי האתרים לא יוכלו להשתמש באפשרות זו בכלל.','%s is a required feature toggle argument.'=>'ערך %s נחוץ עבור מתג האפשרות.','This feature has been disabled by the network admin.'=>'אפשרות זו הושבתה על ידי מנהל הרשת.','Focus keyphrase not set.'=>'לא הוגדרו ביטוי מפתח למיקוד.','Get %s'=>'קבל %s','There is a new notification.'=>'ישנה הודעה חדשה' . "\0" . 'ישנן הודעות חדשות','Colon'=>'נקודתיים','Both %1$s and %2$s manage the SEO of your site. Running two SEO plugins at the same time is detrimental.'=>'גם %1$s וגם %2$s מנהלים את ה-SEO של האתר. הפעלת שני תוספי SEO במקביל יכול להזיק לאתר.','%d minute'=>'דקה %d' . "\0" . '%d דקות','%d hour'=>'שעה %d' . "\0" . '%d שעות','%d day'=>'יום %d' . "\0" . '%d ימים','block descriptionCreate a How-to guide in an SEO-friendly way. You can only use one How-to block per post.'=>'צור מדריך בצורה ידידותית לקידום אתרים. ניתן להשתמש רק בבלוק הדרכה אחד בכל פוסט.','%1$s Structured Data Blocks'=>'בלוק מבנה נתונים של %1$s','Time needed:'=>'זמן נדרש:','Check links to this URL'=>'בדיקת קישורים בכתובת זו','How to'=>'איך','Restore Site'=>'שיחזור אתר','Network Settings'=>'הגדרות רשת','You are not allowed to perform this action.'=>'אינך מורשה לבצע פעולה זו.','Error: %s'=>'שגיאה: %s','Success: %s'=>'הצלחה: %s','Site with ID %d not found.'=>'אתר עם מזהה %d לא נמצא.','No site has been selected to restore.'=>'לא נבחר אתר לשחזור.','You are not allowed to modify unregistered network settings.'=>'אינך מורשה לשנות הגדרות רשת לא רשומות.','deleted'=>'נמחק','The site\'s tagline'=>'תיאור האתר','Not all required fields are given. Missing field %1$s'=>'לא סופקו כל השדות הנדרשים. חסר שדה %1$s','Current year'=>'שנה נוכחית','Tagline'=>'תיאור האתר','Page'=>'עמוד','description (custom taxonomy)'=>'תיאור (טקסונומיה מותאמת)','(custom taxonomy)'=>'(טקסונומיה מותאמת)','(custom field)'=>'(שדה מותאם)','Term404'=>'מונח 404','Caption'=>'כיתוב תמונה','Pagenumber'=>'מספר עמוד','Pagetotal'=>'סה"כ דף','User description'=>'תיאור משתמש','ID'=>'מזהה','Modified'=>'שונה','Post type (plural)'=>'סוג פוסט (רבים)','Post type (singular)'=>'סוג פוסט (יחיד)','Separator'=>'מפריד','Search phrase'=>'ביטוי חיפוש','Term title'=>'כותרת מונח','Term description'=>'תיאור מונח','Tag description'=>'תיאור תגית','Category description'=>'תיאור קטגוריה','Primary category'=>'קטגוריה ראשית','Category'=>'קטגוריה','Tag'=>'תגית','Excerpt only'=>'תיאור בלבד','Excerpt'=>'תיאור','Site title'=>'שם האתר','Archive title'=>'כותרת ארכיון','Parent title'=>'כותרת האב','Date'=>'תאריך','You should create a redirect to ensure your visitors do not get a 404 error when they click on the no longer working URL.'=>'יש ליצור כתובת אתר להפניה מחדש כדי להבטיח שהמבקרים באתר לא יקבלו שגיאת 404 כאשר הם לוחצים על כתובת אתר שאינה פועלת עוד.','Search engines and other websites can still send traffic to your deleted content.'=>'מנועי חיפוש ואתרים אחרים עדיין יכולים לשלוח תנועה לתוכן שלך שנמחק.','Make sure you don\'t miss out on traffic!'=>'ודא שאתה לא מחמיץ את התנועה!','You just deleted a %1$s.'=>'כרגע מחקת את %1$s.','You just trashed a %1$s.'=>'כרגע העברת לאשפה את %1$s.','The %s importer functionality uses temporary database tables. It seems your WordPress install does not have the capability to do this, please consult your hosting provider.'=>'פונקציית היבוא של %s משתמשת בטבלאות זמניות במסדי נתונים. נראה שהתקנת הוורדפרס באתר זה אינה יכולה לעמוד בעומס לביצוע המשימה, יש להתייעץ עם חברת האכסון.','Cleanup of %s data failed.'=>'ניקוי מידע עבור %s נכשל','Content Type'=>'סוג התוכן','Filter by content type'=>'סנן לפי סוגי תוכן','Show All Content Types'=>'הצג את כל סוגי התכנים','Replaced with the normal title for an archive generated by WordPress'=>'הוחלף בכותרת הרגילה של ארכיון שנוצר על ידי וורדפרס','Clean'=>'נקה','Once you\'re certain your site is OK, you can clean up. This will remove all the original data.'=>'כשאתם בטוחים שהאתר בסדר, ניתן לנקות. פעולה זו תסיר את כל המידע המקורי.','Step 5: Clean up'=>'שלב 5: ניקוי','Please check your posts and pages and see if the metadata was successfully imported.'=>'יש לבדוק הפוסטים והעמודים כדי לוודא שהמטה דטה יובא בהצלחה.','Step 3: Check your data'=>'שלב 3: בדוק את המידע','This will import the post metadata like SEO titles and descriptions into your %1$s metadata. It will only do this when there is no existing %1$s metadata yet. The original data will remain in place.'=>'פעולה זו תייבא את נתונים המטא של הפוסט כמו כותרות SEO ותיאורי SEO אל תוך נתוני המטא של %1$s. היא תעשה זאת רק כשאין עדיין מטא נתונים קיימים של %1$s. הנתונים המקוריים יישארו במקומם.','Step 2: Import'=>'שלב 2: יבוא','Please make a backup of your database before starting this process.'=>'אנא הכן גיבוי לבסיס הנתונים לפני תחילת התהליך.','Step 1: Create a backup'=>'שלב 1: צור גיבוי','We\'ve detected data from one or more SEO plugins on your site. Please follow the following steps to import that data:'=>'התגלה מידע מאחד או יותר תוספי SEO באתר. בכדי לייבא את המידע, אנא בצעו את השלבים הבאים:','Plugin: '=>'תוסף:','%s did not detect any plugin data from plugins it can import from.'=>'%s לא זיהה מידע על תוספים מהם ניתן לייבא מידע.','Posts that should not show up in search results'=>'פוסטים שלא אמורים להופיע בתוצאות החיפוש','%s data found.'=>'%s מידע נמצא.','%s data successfully removed.'=>'%s מידע נמחק בהצלחה.','%s data successfully imported.'=>'הגדרות מ-%s יובאו בהצלחה.','%s data not found.'=>'%s מידע לא נמצא.','On'=>'מופעל','Help on: %s'=>'עזרה ב: %s','Off'=>'מכובה','Read why XML Sitemaps are important for your site.'=>'קרא מדוע מפות XML חשובות עבור האתר.','Enable the XML sitemaps that %s generates.'=>'הפעל יצירת מפות XML ש-%s יוצר.','See the XML sitemap.'=>'צפיה במפת XML של האתר.','See who contributed to %1$s.'=>'ראה מי תרם ל-%1$s.','Should search engines follow links on this %1$s?'=>'האם מנועי חיפוש צריכים לעקוב אחרי הקישורים ב%1$s?','Default for %2$s, currently: %1$s'=>'ברירת מחדל עבור %2$s, נוכחי: %1$s','Allow search engines to show this %s in search results?'=>'האם לאפשר למנועי חיפוש להציג %s בתוצאות החיפוש?','Show %s in search results?'=>'הצג %s במנועי חיפוש','Toggle %1$s\'s XML Sitemap'=>'פתח מפות XML של %1$s','%s integration'=>'הרחבת %s','Find out how the text link counter can enhance your SEO.'=>'גלו כיצד מונה הקישור בתכנים יכול לשפר את SEO.','The text link counter helps you improve your site structure.'=>'מונה קישור בתוכן מסייע לשפר את מבנה האתר.','Find out how cornerstone content can help you improve your site structure.'=>'מצא כיצד עוגני תוכן יכולים לעזור לשפר את מבנה האתר.','The cornerstone content feature lets you to mark and filter cornerstone content on your website.'=>'עוגני תוכן מאפשרים לסמן ולסנן עוגני תוכן באתר.','Discover why readability is important for SEO.'=>'גלה למה קריאוּת חשובה ל-SEO.','The readability analysis offers suggestions to improve the structure and style of your text.'=>'ניתוח הקריאוּת מציע הצעות לשיפור המבנה והסגנון של תכני האתר.','Learn how the SEO analysis can help you rank.'=>'למד איך ניתוח SEO יכול לעזור להעלות בדירוג.','The SEO analysis offers suggestions to improve the SEO of your text.'=>'ניתוח SEO מציע הצעות לשיפור SEO עבור תכני האתר.','SEO analysis'=>'ניתוח SEO','https://yoa.st/1uk'=>'https://yoa.st/1uk','https://yoa.st/1uj'=>'https://yoa.st/1uj','Latest blog posts on %1$s'=>'פוסטים אחרונים מהבלוג של %1$s','First-time SEO configuration'=>'הגדרה ראשונה של SEO','%s file'=>'קובץ %s','Save changes to %s'=>'שמור שינויים לקובץ %s','Edit the content of your %s:'=>'עריכת התוכן של הקובץ %s:','Create %s file'=>'צור קובץ %s','Updated %s'=>'%s עודכן','You cannot edit the %s file.'=>'לא ניתן לערוך את הקובץ %s.','You cannot create a %s file.'=>'לא ניתן ליצור את הקובץ %s.','More information about %1$s'=>'מידע נוסף אודות %1$s','Old Configuration Wizard'=>'אשף ההתקנה ישן','Mark the most important %1$s as \'cornerstone content\' to improve your site structure. %2$sLearn more about cornerstone content%3$s.'=>'סמן את ה%1$s החשובים ביותר כעוגני תוכן לשיפור מבנה האתר. %2$sמידע נוסף על עוגני תוכן%3$s.','(Opens in a new browser tab)'=>'(פתח בטאב חדש בדפדפן)','Posts %1$swithout%2$s a focus keyphrase'=>'פוסטים %1$s ללא %2$s ביטוי מפתח למיקוד','Hey, your SEO is doing pretty well! Check out the stats:'=>'דירוג ה-SEO של האתר מצוין! בדוק כאן:','You don\'t have any published posts, your SEO scores will appear here once you make your first post!'=>'עדיין לא פורסמו פוסטים, ציון ה-SEO יופיע כאן ברגע שהפוסט הראשון יהיה באוויר!','Read more like this on our SEO blog'=>'קרא עוד על הנושא בבלוג ה-SEO שלנו','Readability: %s'=>'קריאוּת: %s','%1$s recommendations for you'=>'%1$s המלצות עבורך','All Readability Scores'=>'כל מדדי הקריאוּת','Filter by Readability Score'=>'סינון לפי מדד קריאוּת','Request method %1$s is not valid.'=>'המתודה הנדרשת %1$s אינה תקינה.','Text link counter'=>'מונה קישורים בטקסט','%s Columns'=>'%s עמודות','Readability score'=>'ציון קריאוּת','Cornerstone content'=>'עוגני תוכן','24/7 support'=>'תמיכה 24/7','Save changes'=>'שמור שינויים','1 year free support and updates included!'=>'כולל שנה חינם של עדכונים ושדרוגים!','The plugin %2$s changes your site\'s output and in doing that differentiates between search engines and normal users, a process that\'s called cloaking. We highly recommend that you disable it.'=>'התוסף %2$s משנה את פלט האתר שלך, ובתוך כך מבדיל בין מנועי חיפוש לבין משתמשים רגילים. תהליך זה נקרא "Cloaking". אנו ממליצים לך מאוד לכבות אותו.','Scroll to see the table content.'=>'גלול לצפיה בתוכן הטבלה.','No new notifications.'=>'אין הודעות חדשות','Save all'=>'שמור הכל','Save'=>'שמירה','%1$s, Author at %2$s'=>'%1$s, מחבר ב-%2$s','Name'=>'שם','Export settings'=>'ייצא הגדרות','Please don\'t show me this notification anymore'=>'נא לא להציג יותר התראה זו','If you are experiencing issues, %1$splease file a bug report%2$s and we\'ll do our best to help you out.'=>'אם אתה חווה בעיות, %1$sאנא מלא דו"ח תקלות%2$s ונעשה כמיטב יכולתנו על מנת לעזור לך.','We\'ve noticed you\'ve been using %1$s for some time now; we hope you love it! We\'d be thrilled if you could %2$sgive us a 5 stars rating on WordPress.org%3$s!'=>'שמנו לב שאתה משתמש ב- %1$s כבר במשך זמן מה; אנחנו מקווים שאתה אוהב אותו! נשמח אם תוכל %2$sלתת לנו דירוג של 5 כוכבים ב- WordPress.org%3$s!','Warning: the variable %1$s cannot be used in this template. See the %2$s for more info.'=>'המשתנה %1$s לא ניתן לשימוש בתבנית זו. למידע נוסף ראו %2$s.','By the way, did you know we also have a %1$sPremium plugin%2$s? It offers advanced features, like a redirect manager and support for multiple keyphrases. It also comes with 24/7 personal support.'=>'דרך אגב, ידעת שיש לנו גם %1$sתוסף פרימיום%2$s? הוא מציע מאפיינים מתקדמים, כמו ניהול הפניות ותמיכה במספר ביטויי מפתח. כמו כן, הוא מגיע עם תמיכה אישית 24/7.','(no title)'=>'(ללא כותרת)','The %1$s admin bar menu contains useful links to third-party tools for analyzing pages and makes it easy to see if you have new notifications.'=>'תפריט סרגל הניהול של %1$s מכיל קישורים שימושיים לכלים של צד שלישי לצורך ניתוח עמודים והוא מאפשר לכם לראות בקלות אם יש לכם הודעות חדשות.','Admin bar menu'=>'תפריט בסרגל הניהול','Features'=>'מאפיינים','SEO title'=>'כותרת SEO','Greater than sign'=>'סימן גדול מ-','Less than sign'=>'סימן פחות מ...','Right angle quotation mark'=>'סימן ציטוט זוויתי ימני','Left angle quotation mark'=>'סימן ציטוט זוויתי שמאלי','Small tilde'=>'טילדה קטנה','Vertical bar'=>'קו אנכי','Low asterisk'=>'כוכבית נמוכה','Asterisk'=>'כוכבית','Bullet'=>'נקודה','Middle dot'=>'נקודה אמצעית','Em dash'=>'מפריד Em','En dash'=>'מפריד En','Dash'=>'מפריד','No'=>'לא','Yes'=>'כן','Posts list'=>'רשימת פוסטים','Posts list navigation'=>'ניווט ברשימת הפוסטים','Filter posts list'=>'סינון רשימת פוסטים','Edit “%s”'=>'ערוך את “%s”','Premium'=>'Premium','Get Premium'=>'קבל Premium','Notifications'=>'התראות','Removes the focus keyphrase section from the metabox and disables all SEO-related suggestions.'=>'מסיר את ביטוי מפתח למיקוד מהמטאבוקס ומנטרל את כל המלצות ה-SEO.','Disable SEO analysis'=>'בטל ניתוח SEO','Make primary'=>'הגדר כראשי','%s notification'=>'התראה %s' . "\0" . '%s התראות','Disable readability analysis'=>'בטל ניתוח קריאוּת','Removes the readability analysis section from the metabox and disables all readability-related suggestions.'=>'מסיר את כרטיסיית ניתוח קריאוּת מהמטאבוקס ומבטל את כל ההצעות הקשורות לקריאוּת.','Readability analysis'=>'ניתוח קריאוּת','Needs improvement'=>'דרוש שיפור','Readability'=>'קריאוּת','Good job! We could detect no serious SEO problems.'=>'עבודה טובה! לא נמצאו בעיות SEO משמעותיות.','We have detected the following issues that affect the SEO of your site.'=>'מצאנו את הסוגיות הבאות שמשפיעות על ה-SEO של אתרך.','Problems'=>'בעיות','Not available'=>'לא זמין','Filter by SEO Score'=>'סינון לפי ציון SEO','Meta description not set.'=>'תיאור מטא טרם נקבע.','Dashboard'=>'לוח בקרה','You can fix this on the %1$sPermalink settings page%2$s.'=>'ניתן לתקן זאת דרך %1$sהגדרות מבנה קישורים%2$s.','Replaced with the primary category of the post/page'=>'הוחלף בקטגוריה הראשית של הפוסט / העמוד','New %1$s Title'=>'כותרת %1$s חדשה','Existing %1$s Title'=>'כותרת %1$s קיימת','Expected an integer as input.'=>'ערך מספרי שלם מצופה כקלט','Trying to build the sitemap cache key, but the postfix and prefix combination leaves too little room to do this. You are probably requesting a page that is way out of the expected range.'=>'ניסינו לבנות את מפתח המטמון למפת אתר, אך השילוב של תחילית וסופית משאיר מעט מדי מקום מכדי לעשות זאת. ככל הנראה העמוד שנמצא מעבר לטווח המצופה.','Redirects'=>'הפניות','Remove'=>'הסר','Keep'=>'שמור','Primary %s'=>'%s ראשי','Primary'=>'ראשי','Make %1$s primary %2$s'=>'הפוך את %1$s ל%2$s הראשי','Integrations'=>'הרחבות','Term is set to noindex.'=>'המונח מוגדר כ-noindex.','Enabled'=>'פעיל','Disabled'=>'מכובה','The separator defined in your theme\'s %s tag.'=>'הקו מפריד שהוגדר בתבנית שלך %s תגית.','No index'=>'No index','SEO score'=>'ציון SEO','Team Yoast'=>'צוות Yoast','The first true all-in-one SEO solution for WordPress, including on-page content analysis, XML sitemaps and much more.'=>'פתרון ה-SEO הכולל הראשון עבור וורדפרס, הכולל ניתוח עמוד, מפות אתר ועוד.','Yoast SEO'=>'Yoast SEO','The %1$s plugin installation is incomplete. Please refer to %2$sinstallation instructions%3$s.'=>'התקנת התוסף %1$s לא הושלמה. אנא פנה אל %2$sהוראות ההתקנה%3$s.','The Standard PHP Library (SPL) extension seem to be unavailable. Please ask your web host to enable it.'=>'נראה כי ההרחבה Standard PHP Library (SPL) אינה זמינה. נא לפנות לחברת האחסון להפעיל אותה.','SEO Settings'=>'הגדרות SEO','Google Page Speed Test'=>'בדיקת מהירות טעינת עמוד','Facebook Debugger'=>'פייסבוק דבאגר','Analyze this page'=>'נתח עמוד זה','%s Archive'=>'ארכיון %s','You searched for %s'=>'חיפוש עבור %s','The post %1$s appeared first on %2$s.'=>'הפוסט %1$s הופיע לראשונה ב-%2$s.','No numeric value was received.'=>'לא התקבל ערך מספרי.','This must be an existing blog. Blog %s does not exist or has been marked as deleted.'=>'זה חייב להיות אתר קיים. האתר %s לא קיימת או סומן כנמחק.','The default blog setting must be the numeric blog id of the blog you want to use as default.'=>'הגדרת ברירת המחדל של האתר צריכה להיות ערך מספרי של האתר בו יש להשתמש כברירת מחדל.','%1$s is not a valid choice for who should be allowed access to the %2$s settings. Value reset to the default.'=>'%1$s לא ניתן לאפשר גישה לבחירה זו %2$s. הבחירה אופסה לברירת מחדל.','Please select a valid post type for taxonomy "%s"'=>'נא לבחור סוג תוכן לטקסונומיה "%s"','Please select a valid taxonomy for post type "%s"'=>'נא לבחור טקסונומיה לתוכן מסוג "%s"','You searched for'=>'חיפשת','Home'=>'דף הבית','Archives for'=>'ארכיון עבור','Error 404: Page not found'=>'שגיאה 404: הדף לא נמצא','Good'=>'טוב','Replaced with a custom taxonomies description'=>'הוחלף בתיאור טקסונומיה','Replaced with a posts custom taxonomies, comma separated.'=>'הוחלף בטקסונומיות פוסטים מופרדים בפסיקים.','Replaced with a posts custom field value'=>'הוחלף בערך של השדה המיוחד','Replaced with the slug which caused the 404'=>'הוחלף במזהה כתובת שיצר שגיאת 404','Replaced with the posts focus keyphrase'=>'הוחלף בביטוי מפתח למיקוד של הפוסטים','Attachment caption'=>'מצורף הכיתוב','Replaced with the current page number'=>'הוחלף במספר העמוד הנוכחי','Replaced with the current page total'=>'הוחלף במספר העמודים הכולל','Replaced with the current page number with context (i.e. page 2 of 4)'=>'הוחלף במספר העמוד הנוכחי כולל מלל (למשל עמוד 2 מתוך 4)','Replaced with the current year'=>'השנה הנוכחית','Replaced with the current month'=>'החודש הנוכחי','Replaced with the current day'=>'היום הנוכחי','Replaced with the current date'=>'התאריך הנוכחי','Replaced with the post/page author\'s \'Biographical Info\''=>'הוחלף בביוגרפיה של מחבר הפוסט/עמוד','Replaced with the post/page author\'s \'nicename\''=>'הוחלף בכינוי של מחבר העמוד/פוסט','Replaced with the post/page ID'=>'הוחלף במזהה של הפוסט/עמוד','Replaced with the post/page modified time'=>'הוחלף בשעת עדכון הפוסט/עמוד','Replaced with the content type plural label'=>'הוחלף בשם של סוג התוכן בלשון רבים','Replaced with the content type single label'=>'הוחלף בשם של סוג התוכן בלשון יחיד','Replaced with the current search phrase'=>'הוחלף בביטוי החיפוש הנוכחי','Replaced with the term name'=>'הוחלף בשם המונח','Replaced with the term description'=>'הוחלף בתיאור המונח','Replaced with the tag description'=>'הוחלף בתיאור התגית','Replaced with the category description'=>'הוחלף בתיאור הקטגוריה','Replaced with the post categories (comma separated)'=>'הוחלף בקטגוריות הפוסט (מופרדות בפסיקים)','Replaced with the current tag/tags'=>'הוחלף בתגיות הנוכחיות','Replaced with the post/page excerpt (without auto-generation)'=>'הוחלף בתקציר הפוסט/עמוד (ללא יצירה אוטומטית)','Replaced with the post/page excerpt (or auto-generated if it does not exist)'=>'הוחלף בתקציר הפוסט/עמוד (או שייווצר באופן אוטומטי אם לא קיים)','The site\'s name'=>'שם האתר','Replaced with the title of the parent page of the current page'=>'הוחלף בכותרת של עמוד-אב','Replaced with the title of the post/page'=>'הוחלף בכותרת הפוסט/עמוד','Replaced with the date of the post/page'=>'הוחלף בתאריך הפוסט/עמוד','Page %1$d of %2$d'=>'עמוד %1$d מתוך %2$d','You cannot overrule a WPSEO standard variable replacement by registering a variable with the same name. Use the "wpseo_replacements" filter instead to adjust the replacement value.'=>'לא ניתן שנות משתנה WPSEO על ידי רישום משתנה באותו שם, יש להשתמש בפילטר "wpseo_replacements".','A replacement variable with the same name has already been registered. Try making your variable name unique.'=>'כבר נרשם משתנה החלפה עם שם זהה. יש לבחור שם אחר למשתנה.','A replacement variable can not start with "%%cf_" or "%%ct_" as these are reserved for the WPSEO standard variable variables for custom fields and custom taxonomies. Try making your variable name unique.'=>'משתנה החלפה לא יכול להתחיל עם "%%cf_" או עם "%%ct_" משום שתחיליות אלה שמורות למשתני ברירת המחדל של התוסף לתכנים מותאמים וטקסונומיות. יש לבחור שם אחר למשתנה.','A replacement variable can only contain alphanumeric characters, an underscore or a dash. Try renaming your variable.'=>'משתנה החלפה יכול להכיל רק תווים אלפאנומריים, קו תחתון או במקף. יש לשנות את המשתנה.','Admin only notice: this page does not show a meta description because it does not have one, either write it for this page specifically or go into the [%1$s - %2$s] menu and set up a template.'=>'מנהל שים לב: דף זה לא מציג תיאור מטא בגלל שאין לו תיאור. ניתן לכתוב תיאור ספציפי לעמוד זה או לגשת לתפריט [%1$s - %2$s] וליצור תיאור גנרי.','Page not found'=>'העמוד לא נמצא','%s Archives'=>'ארכיון %s','Meta description to use for Author page'=>'תיאור מטא בדפי מחברים','Title to use for Author page'=>'כותרת בדף מחבר','%1$s settings'=>'הגדרות %1$s','Export your %1$s settings'=>'ייצוא הגדרות %1$s','Import settings'=>'ייבוא ​​הגדרות','Import from other SEO plugins'=>'ייבוא הגדרות מתוסף SEO אחר','Import'=>'ייבוא','If you had a %s file and it was editable, you could edit it from here.'=>'אם לאתר היה קובץ %s והוא היה פתוח לכתיבה, ניתן היה לערוך אותו מכאן.','If your %s were writable, you could edit it from here.'=>'אם הקובץ %s היה פתוח לכתיבה, ניתן היה לערוך אותו מכאן.','You don\'t have a %s file, create one here:'=>'לאתר אין קובץ %s, צור קובץ:','Below are your published posts\' SEO scores. Now is as good a time as any to start improving some of your posts!'=>'בהמשך תמצאו את ציוני ה-SEO עבור הפוסט. זה זמן מצוין להתחיל לשפר את הפוסטים.','Credits'=>'קרדיטים','« Back to Tools page'=>'חזרה לעמוד כלים','%1$s comes with some very powerful built-in tools:'=>'%1$s מגיע עם כמה כלים מובנים חזקים מאוד:','This tool allows you to quickly change important files for your SEO, like your robots.txt and, if you have one, your .htaccess file.'=>'כלי זה מאפשר לשנות בקלות קבצים חשובים לשיפור ה-SEO, לדוגמה קובץ robots.txt וקובץ .htaccess.','File editor'=>'עריכת קבצים','Import settings from other SEO plugins and export your settings for re-use on (another) site.'=>'ייבוא הגדרות מתוספי SEO אחרים או ייצוא הגדרות לשימוש באתר אחר.','Import and Export'=>'ייבוא וייצוא','This tool allows you to quickly change titles and descriptions of your posts and pages without having to go into the editor for each page.'=>'כלי זה מאפשר לך לבצע שינויים בכותרות ובתיאורי עמודים ומאמרים במהירות ללא צורך בכניסה לעורך טקסט של כל עמוד בנפרד.','Bulk editor'=>'עריכה קבוצתית','Default settings'=>'הגדרות ברירת מחדל','Description'=>'תאור','Restore site to defaults'=>'שיחזור ברירת המחדל של האתר','Site ID'=>'ה-ID של האתר','Using this form you can reset a site to the default SEO settings.'=>'באמצעות טופס זה ניתן לשחזר את הגדרות ברירת המחדל.','Privacy sensitive (FB admins and such), theme specific (title rewrite) and a few very site specific settings will not be imported to new sites.'=>'הגדרות רגישות לפרטיות (מנהלי פייסבוק וכאלה), הגדרות ספציפי לתבנית (שכתוב כותרות) וכמה הגדרות ספציפיות אחרות לא ייובאו לאתרים חדשים.','Enter the %1$sSite ID%2$s for the site whose settings you want to use as default for all sites that are added to your network. Leave empty for none (i.e. the normal plugin defaults will be used).'=>'יש להכניס את %1$sמזהה האתר%2$s שבהגדרותיו תרצה להשתמש כברירת מחדל עבור כל האתרים שהוספו לרשת. השאר ריק אם אין אתר כזה (במצב זה יעשה שימוש בהגדרות ברירת המחדל של התוסף).','Choose the site whose settings you want to use as default for all sites that are added to your network. If you choose \'None\', the normal plugin defaults will be used.'=>'יש לבחור את האתר שהגדרותיו ישמשו כהגדרות ברירת מחדל לכל אחר חדש שיתווסף לרשת. אם לא יבחר אתר מסוים, יעשה שימשו בהגדרות ברירת המחדל של התוסף.','New sites in the network inherit their SEO settings from this site'=>'אתרים חדשים ברשת יורשים את הגדרות ה-SEO שלהם מאתר זה.','Super Admins only'=>'סופר מנהלים בלבד','Site Admins (default)'=>'מנהלי אתר (ברירת מחדל)','Who should have access to the %1$s settings'=>'קבע למי תהייה גישה להגדרות %1$s ','spam'=>'ספאם','mature'=>'בוגר','archived'=>'אורכב','public'=>'ציבורי','%s restored to default SEO settings.'=>'%s שיחזרו להגדרות SEO ברירת מחדל.','Settings Updated.'=>'עדכון הגדרות.','Title'=>'כותרת','Take note:'=>'רשום הערה:','Seamlessly integrate WooCommerce with %1$s and get extra features!'=>'שלבו בצורה חלקה את ווקומרס עם %1$s וקבלו אפשרויות נוספות!','Rank better locally and in Google Maps, without breaking a sweat!'=>'דירוג גבוה יותר בחיפוש מקומי ובגוגל מפות, בלי לשבור את הראש!','Are you in Google News? Increase your traffic from Google News by optimizing for it!'=>'האם האתר מופיע בגוגל חדשות? הגדילו את התנועה מגוגל חדשות על ידי אופטימיזציה יעודית!','Optimize your videos to show them off in search results and get more clicks!'=>'אופטימיזציה לוידאו תחשוף אותם בתוצאות מנועי חיפוש ותייצר יותר הקלקות!','The premium version of %1$s with more features & support.'=>'גרסת פרימיום של %1$s עם תמיכה ומאפיינים נוספים.','Person'=>'אדם','Permalink'=>'קישור ישיר','To be able to create a redirect and fix this issue, you need %1$s. You can buy the plugin, including one year of support and updates, on %2$s.'=>'כדי שיתאפשר לכם לבצע הפניה ולתקן את הבעיה בזו, אתם צריכים %1$s. אתם יכולים לרכוש את התוסף, כולל שנה של תמיכה תכנית ועדכונים, ב- %2$s.','Creating redirects is a %s feature'=>'יצירת הפניות היא אפשרות של %s','Deactivate %s'=>'כיבוי %s','The %1$s plugin might cause issues when used in conjunction with %2$s.'=>'התוסף %1$s עלול ליצור בעיות כשמשתמשים בו יחד עם %2$s.','%s Posts Overview'=>'סקירת %s','Close'=>'סגור','Both %1$s and %2$s can create XML sitemaps. Having two XML sitemaps is not beneficial for search engines and might slow down your site.'=>'גם %1$s וגם %2$s יכולים ליצור מפת אתר בפורמט XML. לשתי מפות אתר בפורמט XML אין יתרון מבחינת מנועי חיפוש, והן עלולות להאט את האתר.','Configure %1$s\'s Open Graph settings'=>'הגדר הגדרות Open Graph עבור %1$s','Both %1$s and %2$s create Open Graph output, which might make Facebook, X, LinkedIn and other social networks use the wrong texts and images when your pages are being shared.'=>'גם %1$s וגם %2$s יוצרים פלט Open Graph, שעלול לגרום לפייסבוק, X, LinkedIn ורשתות חברתיות אחרות להשתמש בטקסטים ובתמונות הלא נכונים כאשר הדפים שלך משותפים.','OK'=>'תקין','Meta Desc.'=>'תיאור מטא','All SEO Scores'=>'כל ציוני SEO','Post is set to noindex.'=>'מוגדר לא לאנדקס.','The URL that this page should redirect to.'=>'כתובת ה URL אליו דף זה פונה.','301 Redirect'=>'הפניית 301','The canonical URL that this page should point to. Leave empty to default to permalink. %1$sCross domain canonical%2$s supported too.'=>'כתובת אתר קנונית אליה צריך דף זה להפנות. ניתן להשאיר ריק לברירת המחדל של מבנה הקישורים. תומך גם ב%1$sדומיינים שונים%2$s.','Canonical URL'=>'כתובת אתר קנונית','Title to use for this page in breadcrumb paths'=>'כותרת לשימוש בעמוד זה בנתיב של פירורי לחם','Breadcrumbs Title'=>'כותרת פירורי לחם','No Snippet'=>'אין סניפט','No Archive'=>'אין ארכיון','No Image Index'=>'אין אינדקס תמונה','None'=>'ללא','Meta robots advanced'=>'Meta robots למתקדמים','Warning: even though you can set the meta robots setting here, the entire site is set to noindex in the sitewide privacy settings, so these settings won\'t have an effect.'=>'אזהרה: למרות שניתן להגדיר כאן הגדרות לרובוטים, כל האתר מוגדר noindex בהגדרות הפרטיות, כך שלהגדרות אלה לא תהיינה השפעה.','Meta description'=>'תיאור מטא','SEO Title'=>'כותרת SEO','Focus keyword'=>'מילת מפתח למיקוד','Settings successfully imported.'=>'הגדרות יובאו בהצלחה.','Settings could not be imported:'=>'לא ניתן לייבא את ההגדרות:','Action'=>'פעולה','Page URL/Slug'=>'כתובת/סלאג','Publication date'=>'תאריך פרסום','Post Status'=>'סטטוס','WP Page Title'=>'כותרת של וורדפרס','View'=>'הצג','View “%s”'=>'הצג "%s"','Preview'=>'תצוגה מקדימה','Preview “%s”'=>'תצוגה מקדימה של "%s"','Edit'=>'עריכה','Filter'=>'סנן','postsTrash (%s)'=>'פח (%s)' . "\0" . 'פח (%s)','postsAll (%s)'=>'הכל (%s)' . "\0" . 'הכל (%s)','New Yoast Meta Description'=>'תיאור מטא חדש','Existing Yoast Meta Description'=>'תיאור מטא נוכחי','Facebook profile URL'=>'כתובת פרופיל פייסבוק','FAQ'=>'שאלות נפוצות','Settings'=>'הגדרות','Huge SEO Issue: You\'re blocking access to robots.'=>'בעית SEO רצינית: אתה מונע גישה של רובוטים לאתר.','Posts'=>'פוסטים','Edit Files'=>'עריכת קבצים','General'=>'כללי','Search Console'=>'קונסולת חיפוש','Tools'=>'כלים','XML sitemaps'=>'מפות XML','Social'=>'חברתי','SEO'=>'SEO','%1$s detected you are using version %2$s of %3$s, please update to the latest version to prevent compatibility issues.'=>'%1$s זיהה שאתם משתמשים בגרסה %2$s של %3$s, יש לעדכן לגרסה האחרונה כדי למנוע בעיות תאימות.','Just another WordPress site'=>'עוד אתר וורדפרס','You have used HTML in your value which is not allowed.'=>'השתמשת ב-HTML בערך שהזנת, וזה אסור.','You can\'t edit %s that aren\'t yours.'=>'אינך יכול לערוך %s שלא שלך.','You can\'t edit %s.'=>'אינך יכול לערוך %s.','Post has an invalid Content Type: %s.'=>'לפוסט יש סוג תוכן לא חוקי: %s.','Post doesn\'t exist.'=>'הפוסט אינו קיים.']]; שפחד מופיע,אתה תעשה רק את מה שאתה יודע. - ז.פ. משפטים לחיים

שפחד מופיע,אתה תעשה רק את מה שאתה יודע.

באתר נעשה שימוש בקובצי עוגיות (Cookies) וכלי צד ג' לצורך תפעול האתר, שיפור חוויית הגלישה, ניתוח סטטיסטי (Google Analytics) והתאמת פרסום (Google Ads). באפשרותך לאשר את כל העוגיות, לדחות עוגיות שאינן חיוניות, או לנהל את ההעדפות שלך. למידע נוסף – קרא/י את מדיניות הפרטיות
Call Now Button דילוג לתוכן